1 /**
  2  * Copyright (C) 2013 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global runtime, ops */
 26 
 27 /**
 28  * @constructor
 29  */
 30 ops.OperationTransformMatrix = function OperationTransformMatrix() {
 31     "use strict";
 32 
 33     /* Utility methods */
 34 
 35     /**
 36      * Inverts the range spanned up by the spec's parameter position and length,
 37      * so that position is at the other end of the range and length relative to that.
 38      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
 39      * @return {undefined}
 40      */
 41     function invertMoveCursorSpecRange(moveCursorSpec) {
 42         moveCursorSpec.position = moveCursorSpec.position + moveCursorSpec.length;
 43         moveCursorSpec.length *= -1;
 44     }
 45 
 46     /**
 47      * Inverts the range spanned up by position and length if the length is negative.
 48      * Returns true if an inversion was done, false otherwise.
 49      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
 50      * @return {!boolean}
 51      */
 52     function invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec) {
 53         var isBackwards = (moveCursorSpec.length < 0);
 54 
 55         if (isBackwards) {
 56             invertMoveCursorSpecRange(moveCursorSpec);
 57         }
 58         return isBackwards;
 59     }
 60 
 61     /**
 62      * Returns a list with all attributes in setProperties that refer to styleName
 63      * @param {?odf.Formatting.StyleData} setProperties
 64      * @param {!string} styleName
 65      * @return {!Array.<!string>}
 66      */
 67     function getStyleReferencingAttributes(setProperties, styleName) {
 68         var attributes = [];
 69         /**
 70          * @param {string} attributeName
 71          */
 72         function check(attributeName) {
 73             if (setProperties[attributeName] === styleName) {
 74                 attributes.push(attributeName);
 75             }
 76         }
 77         if (setProperties) {
 78             ['style:parent-style-name', 'style:next-style-name'].forEach(check);
 79         }
 80         return attributes;
 81     }
 82     /**
 83      * @param {?odf.Formatting.StyleData} setProperties
 84      * @param {!string} deletedStyleName
 85      * @return {undefined}
 86      */
 87     function dropStyleReferencingAttributes(setProperties, deletedStyleName) {
 88         /**
 89          * @param {string} attributeName
 90          */
 91         function del(attributeName) {
 92             if (setProperties[attributeName] === deletedStyleName) {
 93                 delete setProperties[attributeName];
 94             }
 95         }
 96         if (setProperties) {
 97             ['style:parent-style-name', 'style:next-style-name'].forEach(del);
 98         }
 99     }
100 
101     /**
102      * Creates a deep copy of the opspec
103      * @param {!Object} opspec
104      * @return {!Object}
105      */
106     function cloneOpspec(opspec) {
107         var result = {};
108 
109         Object.keys(opspec).forEach(function (key) {
110             if (typeof opspec[key] === 'object') {
111                 result[key] = cloneOpspec(opspec[key]);
112             } else {
113                 result[key] = opspec[key];
114             }
115         });
116 
117         return result;
118     }
119 
120     /**
121      * @param {?Object.<string,*>} minorSetProperties
122      * @param {?{attributes:string}} minorRemovedProperties
123      * @param {?Object.<string,*>} majorSetProperties
124      * @param {?{attributes:string}} majorRemovedProperties
125      * @return {!{majorChanged:boolean,minorChanged:boolean}}
126      */
127     function dropOverruledAndUnneededAttributes(minorSetProperties, minorRemovedProperties, majorSetProperties, majorRemovedProperties) {
128         var i, name,
129             majorChanged = false, minorChanged = false,
130             removedPropertyNames,
131             /**@type{!Array.<string>}*/
132             majorRemovedPropertyNames = [];
133         if (majorRemovedProperties && majorRemovedProperties.attributes) {
134             majorRemovedPropertyNames = majorRemovedProperties.attributes.split(',');
135         }
136 
137         // iterate over all properties and see which get overwritten or deleted
138         // by the overruling, so they have to be dropped
139         if (minorSetProperties && (majorSetProperties || majorRemovedPropertyNames.length > 0)) {
140             Object.keys(minorSetProperties).forEach(function (key) {
141                 var value = minorSetProperties[key],
142                     overrulingPropertyValue;
143                 // TODO: support more than one level
144                 if (typeof value !== "object") {
145                     if (majorSetProperties) {
146                         overrulingPropertyValue = majorSetProperties[key];
147                     }
148                     if (overrulingPropertyValue !== undefined) {
149                         // drop overruled
150                         delete minorSetProperties[key];
151                         minorChanged = true;
152 
153                         // major sets to same value?
154                         if (overrulingPropertyValue === value) {
155                             // drop major as well
156                             delete majorSetProperties[key];
157                             majorChanged = true;
158                         }
159                     } else if (majorRemovedPropertyNames.indexOf(key) !== -1) {
160                         // drop overruled
161                         delete minorSetProperties[key];
162                         minorChanged = true;
163                     }
164                 }
165             });
166         }
167 
168         // iterate over all overruling removed properties and drop any duplicates from
169         // the removed property names
170         if (minorRemovedProperties && minorRemovedProperties.attributes && (majorSetProperties || majorRemovedPropertyNames.length > 0)) {
171             removedPropertyNames = minorRemovedProperties.attributes.split(',');
172             for (i = 0; i < removedPropertyNames.length; i += 1) {
173                 name = removedPropertyNames[i];
174                 if ((majorSetProperties && majorSetProperties[name] !== undefined) ||
175                         (majorRemovedPropertyNames && majorRemovedPropertyNames.indexOf(name) !== -1)) {
176                     // drop
177                     removedPropertyNames.splice(i, 1);
178                     i -= 1;
179                     minorChanged = true;
180                 }
181             }
182             // set back
183             if (removedPropertyNames.length > 0) {
184                 minorRemovedProperties.attributes = removedPropertyNames.join(',');
185             } else {
186                 delete minorRemovedProperties.attributes;
187             }
188         }
189 
190         return {
191             majorChanged: majorChanged,
192             minorChanged: minorChanged
193         };
194     }
195 
196     /**
197      * Estimates if there are any properties set in the given properties object.
198      * @param {!odf.Formatting.StyleData} properties
199      * @return {!boolean}
200      */
201     function hasProperties(properties) {
202         var /**@type{string}*/
203             key;
204 
205         for (key in properties) {
206             if (properties.hasOwnProperty(key)) {
207                 return true;
208             }
209         }
210         return false;
211     }
212 
213     /**
214      * Estimates if there are any properties set in the given properties object.
215      * @param {!{attributes:string}} properties
216      * @return {!boolean}
217      */
218     function hasRemovedProperties(properties) {
219         var /**@type{string}*/
220             key;
221 
222         for (key in properties) {
223             if (properties.hasOwnProperty(key)) {
224                 // handle empty 'attribute' as not existing
225                 if (key !== 'attributes' || properties.attributes.length > 0) {
226                     return true;
227                 }
228             }
229         }
230         return false;
231     }
232 
233     /**
234      * @param {?odf.Formatting.StyleData} minorSet
235      * @param {?Object.<string,{attributes:string}>} minorRem
236      * @param {?odf.Formatting.StyleData} majorSet
237      * @param {?Object.<string,{attributes:string}>} majorRem
238      * @param {!string} propertiesName
239      * @return {?{majorChanged:boolean,minorChanged:boolean}}
240      */
241     function dropOverruledAndUnneededProperties(minorSet, minorRem, majorSet, majorRem, propertiesName) {
242         var minorSP = /**@type{?odf.Formatting.StyleData}*/(minorSet ? minorSet[propertiesName] : null),
243             minorRP = minorRem ? minorRem[propertiesName] : null,
244             majorSP = /**@type{?odf.Formatting.StyleData}*/(majorSet ? majorSet[propertiesName] : null),
245             majorRP = majorRem ? majorRem[propertiesName] : null,
246             result;
247 
248         // TODO: also care for nested properties, like there can be e.g. with text:paragraph-properties
249         result = dropOverruledAndUnneededAttributes(minorSP, minorRP, majorSP, majorRP);
250 
251         // remove empty setProperties
252         if (minorSP && !hasProperties(minorSP)) {
253             delete minorSet[propertiesName];
254         }
255         // remove empty removedProperties
256         if (minorRP && !hasRemovedProperties(minorRP)) {
257             delete minorRem[propertiesName];
258         }
259 
260         // remove empty setProperties
261         if (majorSP && !hasProperties(majorSP)) {
262             delete majorSet[propertiesName];
263         }
264         // remove empty removedProperties
265         if (majorRP && !hasRemovedProperties(majorRP)) {
266             delete majorRem[propertiesName];
267         }
268 
269         return result;
270     }
271 
272 
273 
274     /* Transformation methods */
275 
276     /**
277      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpecA
278      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpecB
279      * @param {!boolean} hasAPriority
280      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
281      */
282     function transformAddAnnotationAddAnnotation(addAnnotationSpecA, addAnnotationSpecB, hasAPriority) {
283         var firstAnnotationSpec, secondAnnotationSpec;
284 
285         if (addAnnotationSpecA.position < addAnnotationSpecB.position) {
286             firstAnnotationSpec = addAnnotationSpecA;
287             secondAnnotationSpec = addAnnotationSpecB;
288         } else if (addAnnotationSpecB.position < addAnnotationSpecA.position) {
289             firstAnnotationSpec = addAnnotationSpecB;
290             secondAnnotationSpec = addAnnotationSpecA;
291         } else {
292             firstAnnotationSpec = hasAPriority ? addAnnotationSpecA : addAnnotationSpecB;
293             secondAnnotationSpec = hasAPriority ? addAnnotationSpecB : addAnnotationSpecA;
294         }
295 
296         if (secondAnnotationSpec.position < firstAnnotationSpec.position + firstAnnotationSpec.length) {
297             firstAnnotationSpec.length += 2;
298         }
299         secondAnnotationSpec.position += 2;
300 
301         return {
302             opSpecsA:  [addAnnotationSpecA],
303             opSpecsB:  [addAnnotationSpecB]
304         };
305     }
306 
307     /**
308      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
309      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec
310      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
311      */
312     function transformAddAnnotationApplyDirectStyling(addAnnotationSpec, applyDirectStylingSpec) {
313         if (addAnnotationSpec.position <= applyDirectStylingSpec.position) {
314             applyDirectStylingSpec.position += 2;
315         } else if (addAnnotationSpec.position <= applyDirectStylingSpec.position + applyDirectStylingSpec.length) {
316             applyDirectStylingSpec.length += 2;
317         }
318 
319         return {
320             opSpecsA:  [addAnnotationSpec],
321             opSpecsB:  [applyDirectStylingSpec]
322         };
323     }
324 
325     /**
326      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
327      * @param {!ops.OpInsertText.Spec} insertTextSpec
328      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
329      */
330     function transformAddAnnotationInsertText(addAnnotationSpec, insertTextSpec) {
331         if (insertTextSpec.position <= addAnnotationSpec.position) {
332             addAnnotationSpec.position += insertTextSpec.text.length;
333         } else {
334             if (addAnnotationSpec.length !== undefined) {
335                 if (insertTextSpec.position <= addAnnotationSpec.position + addAnnotationSpec.length) {
336                     addAnnotationSpec.length += insertTextSpec.text.length;
337                 }
338             }
339             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
340             insertTextSpec.position += 2;
341         }
342 
343         return {
344             opSpecsA:  [addAnnotationSpec],
345             opSpecsB:  [insertTextSpec]
346         };
347     }
348 
349     /**
350      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
351      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
352      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
353      */
354     function transformAddAnnotationMergeParagraph(addAnnotationSpec, mergeParagraphSpec) {
355         if (mergeParagraphSpec.sourceStartPosition <= addAnnotationSpec.position) {
356             addAnnotationSpec.position -= 1;
357         } else {
358             if (addAnnotationSpec.length !== undefined) {
359                 if (mergeParagraphSpec.sourceStartPosition <= addAnnotationSpec.position + addAnnotationSpec.length) {
360                     addAnnotationSpec.length -= 1;
361                 }
362             }
363 
364             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
365             mergeParagraphSpec.sourceStartPosition += 2;
366 
367             if (addAnnotationSpec.position < mergeParagraphSpec.destinationStartPosition) {
368                 mergeParagraphSpec.destinationStartPosition += 2;
369             }
370         }
371 
372         return {
373             opSpecsA:  [addAnnotationSpec],
374             opSpecsB:  [mergeParagraphSpec]
375         };
376     }
377 
378     /**
379      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
380      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
381      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
382      */
383     function transformAddAnnotationMoveCursor(addAnnotationSpec, moveCursorSpec) {
384         var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec);
385 
386         // adapt movecursor spec to inserted positions
387         if (addAnnotationSpec.position < moveCursorSpec.position) {
388             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
389             moveCursorSpec.position += 2;
390         } else if (addAnnotationSpec.position < moveCursorSpec.position + moveCursorSpec.length) {
391             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
392             moveCursorSpec.length += 2;
393         }
394 
395         if (isMoveCursorSpecRangeInverted) {
396             invertMoveCursorSpecRange(moveCursorSpec);
397         }
398 
399         return {
400             opSpecsA:  [addAnnotationSpec],
401             opSpecsB:  [moveCursorSpec]
402         };
403     }
404 
405     /**
406      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
407      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
408      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
409      */
410     function transformAddAnnotationRemoveAnnotation(addAnnotationSpec, removeAnnotationSpec) {
411         // adapt movecursor spec to inserted positions
412         if (addAnnotationSpec.position < removeAnnotationSpec.position) {
413             if (removeAnnotationSpec.position < addAnnotationSpec.position + addAnnotationSpec.length) {
414                 addAnnotationSpec.length -= removeAnnotationSpec.length + 2;
415             }
416             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
417             removeAnnotationSpec.position += 2;
418         } else {
419             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
420             addAnnotationSpec.position -= removeAnnotationSpec.length + 2;
421         }
422 
423         return {
424             opSpecsA:  [addAnnotationSpec],
425             opSpecsB:  [removeAnnotationSpec]
426         };
427     }
428 
429     /**
430      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
431      * @param {!ops.OpRemoveText.Spec} removeTextSpec
432      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
433      */
434     function transformAddAnnotationRemoveText(addAnnotationSpec, removeTextSpec) {
435         var removeTextSpecPosition = removeTextSpec.position,
436             removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length,
437             annotationSpecEnd,
438             helperOpspec,
439             addAnnotationSpecResult = [addAnnotationSpec],
440             removeTextSpecResult = [removeTextSpec];
441 
442         // adapt removeTextSpec
443         if (addAnnotationSpec.position <= removeTextSpec.position) {
444             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
445             removeTextSpec.position += 2;
446         } else if (addAnnotationSpec.position < removeTextSpecEnd) {
447             // we have to split the removal into two ops, before and after the annotation start
448             removeTextSpec.length = addAnnotationSpec.position - removeTextSpec.position;
449             helperOpspec = {
450                 optype: "RemoveText",
451                 memberid: removeTextSpec.memberid,
452                 timestamp: removeTextSpec.timestamp,
453                 position: addAnnotationSpec.position + 2,
454                 length: removeTextSpecEnd - addAnnotationSpec.position
455             };
456             removeTextSpecResult.unshift(helperOpspec); // helperOp first, so its position is not affected by the real op
457         }
458 
459         // adapt addAnnotationSpec (using already changed removeTextSpec and new helperOpspec, be aware)
460         if (removeTextSpec.position + removeTextSpec.length <= addAnnotationSpec.position) {
461             addAnnotationSpec.position -= removeTextSpec.length;
462             if ((addAnnotationSpec.length !== undefined) && helperOpspec) {
463                 if (helperOpspec.length >= addAnnotationSpec.length) {
464                     addAnnotationSpec.length = 0;
465                 } else {
466                     addAnnotationSpec.length -= helperOpspec.length;
467                 }
468             }
469         } else if (addAnnotationSpec.length !== undefined) {
470             annotationSpecEnd = addAnnotationSpec.position + addAnnotationSpec.length;
471             if (removeTextSpecEnd <= annotationSpecEnd) {
472                 addAnnotationSpec.length -= removeTextSpec.length;
473             } else if (removeTextSpecPosition < annotationSpecEnd) {
474                 addAnnotationSpec.length = removeTextSpecPosition - addAnnotationSpec.position;
475             }
476         }
477 
478         return {
479             opSpecsA:  addAnnotationSpecResult,
480             opSpecsB:  removeTextSpecResult
481         };
482     }
483 
484     /**
485      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
486      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
487      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
488      */
489     function transformAddAnnotationSetParagraphStyle(addAnnotationSpec, setParagraphStyleSpec) {
490         if (addAnnotationSpec.position < setParagraphStyleSpec.position) {
491             setParagraphStyleSpec.position += 2;
492         }
493 
494         return {
495             opSpecsA:  [addAnnotationSpec],
496             opSpecsB:  [setParagraphStyleSpec]
497         };
498     }
499 
500     /**
501      * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec
502      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
503      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
504      */
505     function transformAddAnnotationSplitParagraph(addAnnotationSpec, splitParagraphSpec) {
506         if (addAnnotationSpec.position < splitParagraphSpec.sourceParagraphPosition) {
507             splitParagraphSpec.sourceParagraphPosition += 2;
508         }
509 
510         if (splitParagraphSpec.position <= addAnnotationSpec.position) {
511             addAnnotationSpec.position += 1;
512         } else {
513             if (addAnnotationSpec.length !== undefined) {
514                 if (splitParagraphSpec.position <= addAnnotationSpec.position + addAnnotationSpec.length) {
515                     addAnnotationSpec.length += 1;
516                 }
517             }
518             // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range
519             splitParagraphSpec.position += 2;
520         }
521 
522         return {
523             opSpecsA:  [addAnnotationSpec],
524             opSpecsB:  [splitParagraphSpec]
525         };
526     }
527 
528     /**
529      * @param {!ops.OpAddStyle.Spec} addStyleSpec
530      * @param {!ops.OpRemoveStyle.Spec} removeStyleSpec
531      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
532      */
533     function transformAddStyleRemoveStyle(addStyleSpec, removeStyleSpec) {
534         var setAttributes,
535             helperOpspec,
536             addStyleSpecResult = [addStyleSpec],
537             removeStyleSpecResult = [removeStyleSpec];
538 
539         if (addStyleSpec.styleFamily === removeStyleSpec.styleFamily) {
540             // deleted style brought into use by addstyle op?
541             setAttributes = getStyleReferencingAttributes(addStyleSpec.setProperties, removeStyleSpec.styleName);
542             if (setAttributes.length > 0) {
543                 // just create a updateparagraph style op preceding to us which removes any set style from the paragraph
544                 helperOpspec = {
545                     optype: "UpdateParagraphStyle",
546                     memberid: removeStyleSpec.memberid,
547                     timestamp: removeStyleSpec.timestamp,
548                     styleName: addStyleSpec.styleName,
549                     removedProperties: { attributes: setAttributes.join(',') }
550                 };
551                 removeStyleSpecResult.unshift(helperOpspec);
552             }
553             // in the addstyle op drop any attributes referencing the style deleted
554             dropStyleReferencingAttributes(addStyleSpec.setProperties, removeStyleSpec.styleName);
555         }
556 
557         return {
558             opSpecsA:  addStyleSpecResult,
559             opSpecsB:  removeStyleSpecResult
560         };
561     }
562 
563     /**
564      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpecA
565      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpecB
566      * @param {!boolean} hasAPriority
567      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
568      */
569     function transformApplyDirectStylingApplyDirectStyling(applyDirectStylingSpecA, applyDirectStylingSpecB, hasAPriority) {
570         var majorSpec, minorSpec, majorSpecResult, minorSpecResult,
571             majorSpecEnd, minorSpecEnd, dropResult,
572             originalMajorSpec, originalMinorSpec,
573             helperOpspecBefore, helperOpspecAfter,
574             applyDirectStylingSpecAResult = [applyDirectStylingSpecA],
575             applyDirectStylingSpecBResult = [applyDirectStylingSpecB];
576 
577         // overlapping and any conflicting attributes?
578         if (!(applyDirectStylingSpecA.position + applyDirectStylingSpecA.length <= applyDirectStylingSpecB.position ||
579               applyDirectStylingSpecA.position >= applyDirectStylingSpecB.position + applyDirectStylingSpecB.length)) {
580             // adapt to priority
581             majorSpec = hasAPriority ? applyDirectStylingSpecA : applyDirectStylingSpecB;
582             minorSpec = hasAPriority ? applyDirectStylingSpecB : applyDirectStylingSpecA;
583 
584             // might need original opspecs?
585             if (applyDirectStylingSpecA.position !== applyDirectStylingSpecB.position ||
586                     applyDirectStylingSpecA.length !== applyDirectStylingSpecB.length) {
587                 originalMajorSpec = cloneOpspec(majorSpec);
588                 originalMinorSpec = cloneOpspec(minorSpec);
589             }
590 
591             // for the part that is overlapping reduce setProperties by the overruled properties
592             dropResult = dropOverruledAndUnneededProperties(
593                 minorSpec.setProperties,
594                 null,
595                 majorSpec.setProperties,
596                 null,
597                 'style:text-properties'
598             );
599 
600             if (dropResult.majorChanged || dropResult.minorChanged) {
601                 // split the less-priority op into several ops for the overlapping and non-overlapping ranges
602                 majorSpecResult = [];
603                 minorSpecResult = [];
604 
605                 majorSpecEnd = majorSpec.position + majorSpec.length;
606                 minorSpecEnd = minorSpec.position + minorSpec.length;
607 
608                 // find if there is a part before and if there is a part behind,
609                 // create range-adapted copies of the original opspec, if the spec has changed
610                 if (minorSpec.position < majorSpec.position) {
611                     if (dropResult.minorChanged) {
612                         helperOpspecBefore = cloneOpspec(/**@type{!Object}*/(originalMinorSpec));
613                         helperOpspecBefore.length = majorSpec.position - minorSpec.position;
614                         minorSpecResult.push(helperOpspecBefore);
615 
616                         minorSpec.position = majorSpec.position;
617                         minorSpec.length = minorSpecEnd - minorSpec.position;
618                     }
619                 } else if (majorSpec.position < minorSpec.position) {
620                     if (dropResult.majorChanged) {
621                         helperOpspecBefore = cloneOpspec(/**@type{!Object}*/(originalMajorSpec));
622                         helperOpspecBefore.length = minorSpec.position - majorSpec.position;
623                         majorSpecResult.push(helperOpspecBefore);
624 
625                         majorSpec.position = minorSpec.position;
626                         majorSpec.length = majorSpecEnd - majorSpec.position;
627                     }
628                 }
629                 if (minorSpecEnd > majorSpecEnd) {
630                     if (dropResult.minorChanged) {
631                         helperOpspecAfter = originalMinorSpec;
632                         helperOpspecAfter.position = majorSpecEnd;
633                         helperOpspecAfter.length = minorSpecEnd - majorSpecEnd;
634                         minorSpecResult.push(helperOpspecAfter);
635 
636                         minorSpec.length = majorSpecEnd - minorSpec.position;
637                     }
638                 } else if (majorSpecEnd > minorSpecEnd) {
639                     if (dropResult.majorChanged) {
640                         helperOpspecAfter = originalMajorSpec;
641                         helperOpspecAfter.position = minorSpecEnd;
642                         helperOpspecAfter.length = majorSpecEnd - minorSpecEnd;
643                         majorSpecResult.push(helperOpspecAfter);
644 
645                         majorSpec.length = minorSpecEnd - majorSpec.position;
646                     }
647                 }
648 
649                 // check if there are any changes left and this op has not become a noop
650                 if (majorSpec.setProperties && hasProperties(majorSpec.setProperties)) {
651                     majorSpecResult.push(majorSpec);
652                 }
653                 // check if there are any changes left and this op has not become a noop
654                 if (minorSpec.setProperties && hasProperties(minorSpec.setProperties)) {
655                     minorSpecResult.push(minorSpec);
656                 }
657 
658                 if (hasAPriority) {
659                     applyDirectStylingSpecAResult = majorSpecResult;
660                     applyDirectStylingSpecBResult = minorSpecResult;
661                 } else {
662                     applyDirectStylingSpecAResult = minorSpecResult;
663                     applyDirectStylingSpecBResult = majorSpecResult;
664                 }
665             }
666         }
667 
668         return {
669             opSpecsA:  applyDirectStylingSpecAResult,
670             opSpecsB:  applyDirectStylingSpecBResult
671         };
672     }
673 
674     /**
675      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec
676      * @param {!ops.OpInsertText.Spec} insertTextSpec
677      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
678      */
679     function transformApplyDirectStylingInsertText(applyDirectStylingSpec, insertTextSpec) {
680         // adapt applyDirectStyling spec to inserted positions
681         if (insertTextSpec.position <= applyDirectStylingSpec.position) {
682             applyDirectStylingSpec.position += insertTextSpec.text.length;
683         } else if (insertTextSpec.position <= applyDirectStylingSpec.position + applyDirectStylingSpec.length) {
684             applyDirectStylingSpec.length += insertTextSpec.text.length;
685         }
686 
687         return {
688             opSpecsA:  [applyDirectStylingSpec],
689             opSpecsB:  [insertTextSpec]
690         };
691     }
692 
693     /**
694      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec
695      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
696      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
697      */
698     function transformApplyDirectStylingMergeParagraph(applyDirectStylingSpec, mergeParagraphSpec) {
699         var pointA = applyDirectStylingSpec.position,
700             pointB = applyDirectStylingSpec.position + applyDirectStylingSpec.length;
701 
702         // adapt applyDirectStyling spec to merged paragraph
703         if (pointA >= mergeParagraphSpec.sourceStartPosition) {
704             pointA -= 1;
705         }
706         if (pointB >= mergeParagraphSpec.sourceStartPosition) {
707             pointB -= 1;
708         }
709         applyDirectStylingSpec.position = pointA;
710         applyDirectStylingSpec.length = pointB - pointA;
711 
712         return {
713             opSpecsA:  [applyDirectStylingSpec],
714             opSpecsB:  [mergeParagraphSpec]
715         };
716     }
717 
718     /**
719      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec
720      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
721      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
722      */
723     function transformApplyDirectStylingRemoveAnnotation(applyDirectStylingSpec, removeAnnotationSpec) {
724         var pointA = applyDirectStylingSpec.position,
725             pointB = applyDirectStylingSpec.position + applyDirectStylingSpec.length,
726             removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length,
727             applyDirectStylingSpecResult = [applyDirectStylingSpec],
728             removeAnnotationSpecResult = [removeAnnotationSpec];
729 
730         // check if inside removed annotation
731         if (removeAnnotationSpec.position <= pointA && pointB <= removeAnnotationEnd) {
732             applyDirectStylingSpecResult = [];
733         } else {
734             // adapt applyDirectStyling spec to removed annotation content
735             if (removeAnnotationEnd < pointA) {
736                 pointA -= removeAnnotationSpec.length + 2;
737             }
738             if (removeAnnotationEnd < pointB) {
739                 pointB -= removeAnnotationSpec.length + 2;
740             }
741             applyDirectStylingSpec.position = pointA;
742             applyDirectStylingSpec.length = pointB - pointA;
743         }
744 
745         return {
746             opSpecsA:  applyDirectStylingSpecResult,
747             opSpecsB:  removeAnnotationSpecResult
748         };
749     }
750 
751     /**
752      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec
753      * @param {!ops.OpRemoveText.Spec} removeTextSpec
754      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
755      */
756     function transformApplyDirectStylingRemoveText(applyDirectStylingSpec, removeTextSpec) {
757         var applyDirectStylingSpecEnd = applyDirectStylingSpec.position + applyDirectStylingSpec.length,
758             removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length,
759             applyDirectStylingSpecResult = [applyDirectStylingSpec],
760             removeTextSpecResult = [removeTextSpec];
761 
762         // transform applyDirectStylingSpec
763         // removed positions by object up to move cursor position?
764         if (removeTextSpecEnd <= applyDirectStylingSpec.position) {
765             // adapt by removed position
766             applyDirectStylingSpec.position -= removeTextSpec.length;
767         // overlapping?
768         } else if (removeTextSpec.position < applyDirectStylingSpecEnd) {
769             // still to select range starting at cursor position?
770             if (applyDirectStylingSpec.position < removeTextSpec.position) {
771                 // still to select range ending at selection?
772                 if (removeTextSpecEnd < applyDirectStylingSpecEnd) {
773                     applyDirectStylingSpec.length -= removeTextSpec.length;
774                 } else {
775                     applyDirectStylingSpec.length = removeTextSpec.position - applyDirectStylingSpec.position;
776                 }
777             // remove overlapping section
778             } else {
779                 // fall at start of removed section
780                 applyDirectStylingSpec.position = removeTextSpec.position;
781                 // still to select range at selection end?
782                 if (removeTextSpecEnd < applyDirectStylingSpecEnd) {
783                     applyDirectStylingSpec.length = applyDirectStylingSpecEnd - removeTextSpecEnd;
784                 } else {
785                     // completely overlapped by other, so becomes no-op
786                     // TODO: once we can address spans, removeTextSpec would need to get a helper op
787                     // to remove the empty span left over
788                     applyDirectStylingSpecResult = [];
789                 }
790             }
791         }
792 
793         return {
794             opSpecsA:  applyDirectStylingSpecResult,
795             opSpecsB:  removeTextSpecResult
796         };
797     }
798 
799     /**
800      * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec
801      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
802      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
803      */
804     function transformApplyDirectStylingSplitParagraph(applyDirectStylingSpec, splitParagraphSpec) {
805         // transform applyDirectStylingSpec
806         if (splitParagraphSpec.position < applyDirectStylingSpec.position) {
807             applyDirectStylingSpec.position += 1;
808         } else if (splitParagraphSpec.position < applyDirectStylingSpec.position + applyDirectStylingSpec.length) {
809             applyDirectStylingSpec.length += 1;
810         }
811 
812         return {
813             opSpecsA:  [applyDirectStylingSpec],
814             opSpecsB:  [splitParagraphSpec]
815         };
816     }
817 
818     /**
819      * @param {!ops.OpInsertText.Spec} insertTextSpecA
820      * @param {!ops.OpInsertText.Spec} insertTextSpecB
821      * @param {!boolean} hasAPriority
822      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
823      */
824     function transformInsertTextInsertText(insertTextSpecA, insertTextSpecB, hasAPriority) {
825         if (insertTextSpecA.position < insertTextSpecB.position) {
826             insertTextSpecB.position += insertTextSpecA.text.length;
827         } else if (insertTextSpecA.position > insertTextSpecB.position) {
828             insertTextSpecA.position += insertTextSpecB.text.length;
829         } else {
830             if (hasAPriority) {
831                 insertTextSpecB.position += insertTextSpecA.text.length;
832             } else {
833                 insertTextSpecA.position += insertTextSpecB.text.length;
834             }
835         }
836 
837         return {
838             opSpecsA:  [insertTextSpecA],
839             opSpecsB:  [insertTextSpecB]
840         };
841     }
842 
843     /**
844      * @param {!ops.OpInsertText.Spec} insertTextSpec
845      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
846      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
847      */
848     function transformInsertTextMergeParagraph(insertTextSpec, mergeParagraphSpec) {
849         if (insertTextSpec.position >= mergeParagraphSpec.sourceStartPosition) {
850             insertTextSpec.position -= 1;
851         } else {
852             if (insertTextSpec.position < mergeParagraphSpec.sourceStartPosition) {
853                 mergeParagraphSpec.sourceStartPosition += insertTextSpec.text.length;
854             }
855             if (insertTextSpec.position < mergeParagraphSpec.destinationStartPosition) {
856                 mergeParagraphSpec.destinationStartPosition += insertTextSpec.text.length;
857             }
858         }
859 
860         return {
861             opSpecsA:  [insertTextSpec],
862             opSpecsB:  [mergeParagraphSpec]
863         };
864     }
865 
866     /**
867      * @param {!ops.OpInsertText.Spec} insertTextSpec
868      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
869      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
870      */
871     function transformInsertTextMoveCursor(insertTextSpec, moveCursorSpec) {
872         var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec);
873 
874         // adapt movecursor spec to inserted positions
875         if (insertTextSpec.position < moveCursorSpec.position) {
876             moveCursorSpec.position += insertTextSpec.text.length;
877         } else if (insertTextSpec.position < moveCursorSpec.position + moveCursorSpec.length) {
878             moveCursorSpec.length += insertTextSpec.text.length;
879         }
880 
881         if (isMoveCursorSpecRangeInverted) {
882             invertMoveCursorSpecRange(moveCursorSpec);
883         }
884 
885         return {
886             opSpecsA:  [insertTextSpec],
887             opSpecsB:  [moveCursorSpec]
888         };
889     }
890 
891     /**
892      * @param {!ops.OpInsertText.Spec} insertTextSpec
893      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
894      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
895      */
896     function transformInsertTextRemoveAnnotation(insertTextSpec, removeAnnotationSpec) {
897         var insertTextSpecPosition = insertTextSpec.position,
898             removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length,
899             insertTextSpecResult = [insertTextSpec],
900             removeAnnotationSpecResult = [removeAnnotationSpec];
901 
902         // check if inside removed annotation
903         if (removeAnnotationSpec.position <= insertTextSpecPosition && insertTextSpecPosition <= removeAnnotationEnd) {
904             insertTextSpecResult = [];
905             removeAnnotationSpec.length += insertTextSpec.text.length;
906         } else {
907             // adapt insertText spec to removed annotation content
908             if (removeAnnotationEnd < insertTextSpec.position) {
909                 insertTextSpec.position -= removeAnnotationSpec.length + 2;
910             } else {
911                 removeAnnotationSpec.position += insertTextSpec.text.length;
912             }
913         }
914 
915         return {
916             opSpecsA:  insertTextSpecResult,
917             opSpecsB:  removeAnnotationSpecResult
918         };
919     }
920 
921     /**
922      * @param {!ops.OpInsertText.Spec} insertTextSpec
923      * @param {!ops.OpRemoveText.Spec} removeTextSpec
924      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
925      */
926     function transformInsertTextRemoveText(insertTextSpec, removeTextSpec) {
927         var helperOpspec,
928             removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length,
929             insertTextSpecResult = [insertTextSpec],
930             removeTextSpecResult = [removeTextSpec];
931 
932         // update insertTextSpec
933         // removed before/up to insertion point?
934         if (removeTextSpecEnd <= insertTextSpec.position) {
935             insertTextSpec.position -= removeTextSpec.length;
936         // removed at/behind insertion point
937         } else if (insertTextSpec.position <= removeTextSpec.position) {
938             removeTextSpec.position += insertTextSpec.text.length;
939         // insertion in middle of removed range
940         } else {
941             // we have to split the removal into two ops, before and after the insertion point
942             removeTextSpec.length = insertTextSpec.position - removeTextSpec.position;
943             helperOpspec = {
944                 optype: "RemoveText",
945                 memberid: removeTextSpec.memberid,
946                 timestamp: removeTextSpec.timestamp,
947                 position: insertTextSpec.position + insertTextSpec.text.length,
948                 length: removeTextSpecEnd - insertTextSpec.position
949             };
950             removeTextSpecResult.unshift(helperOpspec); // helperOp first, so its position is not affected by the real op
951             // drop insertion point to begin of removed range
952             // original insertTextSpec.position is used for removeTextSpec changes, so only change now
953             insertTextSpec.position = removeTextSpec.position;
954         }
955 
956         return {
957             opSpecsA:  insertTextSpecResult,
958             opSpecsB:  removeTextSpecResult
959         };
960     }
961 
962     /**
963      * @param {!ops.OpInsertText.Spec} insertTextSpec
964      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
965      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
966      */
967     function transformInsertTextSetParagraphStyle(insertTextSpec, setParagraphStyleSpec) {
968         if (setParagraphStyleSpec.position > insertTextSpec.position) {
969             setParagraphStyleSpec.position += insertTextSpec.text.length;
970         }
971 
972         return {
973             opSpecsA:  [insertTextSpec],
974             opSpecsB:  [setParagraphStyleSpec]
975         };
976     }
977 
978     /**
979      * @param {!ops.OpInsertText.Spec} insertTextSpec
980      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
981      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
982      */
983     function transformInsertTextSplitParagraph(insertTextSpec, splitParagraphSpec) {
984         if (insertTextSpec.position < splitParagraphSpec.sourceParagraphPosition) {
985             splitParagraphSpec.sourceParagraphPosition += insertTextSpec.text.length;
986         }
987 
988         if (insertTextSpec.position <= splitParagraphSpec.position) {
989             splitParagraphSpec.position += insertTextSpec.text.length;
990         } else {
991             insertTextSpec.position += 1;
992         }
993 
994         return {
995             opSpecsA:  [insertTextSpec],
996             opSpecsB:  [splitParagraphSpec]
997         };
998     }
999 
1000     /**
1001      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpecA
1002      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpecB
1003      * @param {!boolean} hasAPriority
1004      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1005      */
1006     function transformMergeParagraphMergeParagraph(mergeParagraphSpecA, mergeParagraphSpecB, hasAPriority) {
1007         var specsForB = [mergeParagraphSpecA],
1008             specsForA = [mergeParagraphSpecB],
1009             priorityOp,
1010             styleParagraphFixup,
1011             moveCursorA,
1012             moveCursorB;
1013 
1014         if (mergeParagraphSpecA.destinationStartPosition === mergeParagraphSpecB.destinationStartPosition) {
1015             // Two merge commands for the same paragraph result in a noop to both sides, as the same
1016             // paragraph can only be merged once.
1017             specsForB = [];
1018             specsForA = [];
1019             // If the moveCursor flag is set, the cursor will still need to be adjusted to the right location
1020             if (mergeParagraphSpecA.moveCursor) {
1021                 moveCursorA = /**@type{!ops.OpMoveCursor.Spec}*/({
1022                     optype: "MoveCursor",
1023                     memberid: mergeParagraphSpecA.memberid,
1024                     timestamp: mergeParagraphSpecA.timestamp,
1025                     position: mergeParagraphSpecA.sourceStartPosition - 1
1026                 });
1027                 specsForB.push(moveCursorA);
1028             }
1029             if (mergeParagraphSpecB.moveCursor) {
1030                 moveCursorB = /**@type{!ops.OpMoveCursor.Spec}*/({
1031                     optype: "MoveCursor",
1032                     memberid: mergeParagraphSpecB.memberid,
1033                     timestamp: mergeParagraphSpecB.timestamp,
1034                     position: mergeParagraphSpecB.sourceStartPosition - 1
1035                 });
1036                 specsForA.push(moveCursorB);
1037             }
1038 
1039             // Determine which merge style wins
1040             priorityOp = hasAPriority ? mergeParagraphSpecA : mergeParagraphSpecB;
1041             styleParagraphFixup = /**@type{!ops.OpSetParagraphStyle.Spec}*/({
1042                 optype: "SetParagraphStyle",
1043                 memberid: priorityOp.memberid,
1044                 timestamp: priorityOp.timestamp,
1045                 position: priorityOp.destinationStartPosition,
1046                 styleName: priorityOp.paragraphStyleName
1047             });
1048             if (hasAPriority) {
1049                 specsForB.push(styleParagraphFixup);
1050             } else {
1051                 specsForA.push(styleParagraphFixup);
1052             }
1053         } else if (mergeParagraphSpecB.sourceStartPosition === mergeParagraphSpecA.destinationStartPosition) {
1054             // Two consecutive paragraphs are being merged. E.g., A <- B <- C.
1055             // Use the styleName of the lowest destination paragraph to set the paragraph style (A <- B)
1056             mergeParagraphSpecA.destinationStartPosition = mergeParagraphSpecB.destinationStartPosition;
1057             mergeParagraphSpecA.sourceStartPosition -= 1;
1058             mergeParagraphSpecA.paragraphStyleName = mergeParagraphSpecB.paragraphStyleName;
1059         } else if (mergeParagraphSpecA.sourceStartPosition === mergeParagraphSpecB.destinationStartPosition) {
1060             // Two consecutive paragraphs are being merged. E.g., A <- B <- C.
1061             // Use the styleName of the lowest destination paragraph to set the paragraph style (A <- B)
1062             mergeParagraphSpecB.destinationStartPosition = mergeParagraphSpecA.destinationStartPosition;
1063             mergeParagraphSpecB.sourceStartPosition -= 1;
1064             mergeParagraphSpecB.paragraphStyleName = mergeParagraphSpecA.paragraphStyleName;
1065         } else if (mergeParagraphSpecA.destinationStartPosition < mergeParagraphSpecB.destinationStartPosition) {
1066             mergeParagraphSpecB.destinationStartPosition -= 1;
1067             mergeParagraphSpecB.sourceStartPosition -= 1;
1068         } else { // mergeParagraphSpecB.destinationStartPosition < mergeParagraphSpecA.destinationStartPosition
1069             mergeParagraphSpecA.destinationStartPosition -= 1;
1070             mergeParagraphSpecA.sourceStartPosition -= 1;
1071         }
1072 
1073         return {
1074             opSpecsA:  specsForB,
1075             opSpecsB:  specsForA
1076         };
1077     }
1078 
1079     /**
1080      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
1081      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
1082      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1083      */
1084     function transformMergeParagraphMoveCursor(mergeParagraphSpec, moveCursorSpec) {
1085         var pointA = moveCursorSpec.position,
1086             pointB = moveCursorSpec.position + moveCursorSpec.length,
1087             start = Math.min(pointA, pointB),
1088             end = Math.max(pointA, pointB);
1089 
1090         if (start >= mergeParagraphSpec.sourceStartPosition) {
1091             start -= 1;
1092         }
1093         if (end >= mergeParagraphSpec.sourceStartPosition) {
1094             end -= 1;
1095         }
1096 
1097         // When updating the cursor spec, ensure the selection direction is preserved.
1098         // If the length was previously positive, it should remain positive.
1099         if (moveCursorSpec.length >= 0) {
1100             moveCursorSpec.position = start;
1101             moveCursorSpec.length = end - start;
1102         } else {
1103             moveCursorSpec.position = end;
1104             moveCursorSpec.length = start - end;
1105         }
1106 
1107         return {
1108             opSpecsA:  [mergeParagraphSpec],
1109             opSpecsB:  [moveCursorSpec]
1110         };
1111     }
1112 
1113     /**
1114      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
1115      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
1116      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1117      */
1118     function transformMergeParagraphRemoveAnnotation(mergeParagraphSpec, removeAnnotationSpec) {
1119         var removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length,
1120             mergeParagraphSpecResult = [mergeParagraphSpec],
1121             removeAnnotationSpecResult = [removeAnnotationSpec];
1122 
1123         // check if inside removed annotation
1124         if (removeAnnotationSpec.position <= mergeParagraphSpec.destinationStartPosition && mergeParagraphSpec.sourceStartPosition <= removeAnnotationEnd) {
1125             mergeParagraphSpecResult = [];
1126             removeAnnotationSpec.length -= 1;
1127         } else {
1128             if (mergeParagraphSpec.sourceStartPosition < removeAnnotationSpec.position) {
1129                 removeAnnotationSpec.position -= 1;
1130             } else {
1131                 if (removeAnnotationEnd < mergeParagraphSpec.destinationStartPosition) {
1132                     mergeParagraphSpec.destinationStartPosition -= removeAnnotationSpec.length + 2;
1133                 }
1134                 if (removeAnnotationEnd < mergeParagraphSpec.sourceStartPosition) {
1135                     mergeParagraphSpec.sourceStartPosition -= removeAnnotationSpec.length + 2;
1136                 }
1137             }
1138         }
1139 
1140         return {
1141             opSpecsA:  mergeParagraphSpecResult,
1142             opSpecsB:  removeAnnotationSpecResult
1143         };
1144     }
1145 
1146     /**
1147      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
1148      * @param {!ops.OpRemoveText.Spec} removeTextSpec
1149      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1150      */
1151     function transformMergeParagraphRemoveText(mergeParagraphSpec, removeTextSpec) {
1152         // RemoveText ops can't cross paragraph boundaries, so only the position needs to be checked
1153         if (removeTextSpec.position >= mergeParagraphSpec.sourceStartPosition) {
1154             removeTextSpec.position -= 1;
1155         } else {
1156             if (removeTextSpec.position < mergeParagraphSpec.destinationStartPosition) {
1157                 mergeParagraphSpec.destinationStartPosition -= removeTextSpec.length;
1158             }
1159             if (removeTextSpec.position < mergeParagraphSpec.sourceStartPosition) {
1160                 mergeParagraphSpec.sourceStartPosition -= removeTextSpec.length;
1161             }
1162         }
1163 
1164         return {
1165             opSpecsA:  [mergeParagraphSpec],
1166             opSpecsB:  [removeTextSpec]
1167         };
1168     }
1169 
1170     /**
1171      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
1172      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
1173      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1174      */
1175     function transformMergeParagraphSetParagraphStyle(mergeParagraphSpec, setParagraphStyleSpec) {
1176         var opSpecsA = [mergeParagraphSpec],
1177             opSpecsB = [setParagraphStyleSpec];
1178 
1179         // SetParagraphStyle ops can't cross paragraph boundaries
1180         if (setParagraphStyleSpec.position > mergeParagraphSpec.sourceStartPosition) {
1181             // Paragraph beyond the ones region affected by the merge
1182             setParagraphStyleSpec.position -= 1;
1183         } else if (setParagraphStyleSpec.position === mergeParagraphSpec.destinationStartPosition
1184                     || setParagraphStyleSpec.position === mergeParagraphSpec.sourceStartPosition) {
1185             // Attempting to style a merging paragraph
1186             setParagraphStyleSpec.position = mergeParagraphSpec.destinationStartPosition;
1187             mergeParagraphSpec.paragraphStyleName = setParagraphStyleSpec.styleName;
1188         }
1189 
1190         return {
1191             opSpecsA:  opSpecsA,
1192             opSpecsB:  opSpecsB
1193         };
1194     }
1195 
1196     /**
1197      * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec
1198      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
1199      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1200      */
1201     function transformMergeParagraphSplitParagraph(mergeParagraphSpec, splitParagraphSpec) {
1202         var styleSplitParagraph,
1203             moveCursorOp,
1204             opSpecsA = [mergeParagraphSpec],
1205             opSpecsB = [splitParagraphSpec];
1206 
1207         if (splitParagraphSpec.position < mergeParagraphSpec.destinationStartPosition) {
1208             // Split occurs before the merge destination
1209             // Splitting a paragraph inserts one step, moving the merge along
1210             mergeParagraphSpec.destinationStartPosition += 1;
1211             mergeParagraphSpec.sourceStartPosition += 1;
1212         } else if (splitParagraphSpec.position >= mergeParagraphSpec.destinationStartPosition
1213             && splitParagraphSpec.position < mergeParagraphSpec.sourceStartPosition) {
1214             // split occurs within the paragraphs being merged
1215             splitParagraphSpec.paragraphStyleName = mergeParagraphSpec.paragraphStyleName;
1216             styleSplitParagraph = /**@type{!ops.OpSetParagraphStyle.Spec}*/({
1217                 optype: "SetParagraphStyle",
1218                 memberid: mergeParagraphSpec.memberid,
1219                 timestamp: mergeParagraphSpec.timestamp,
1220                 position: mergeParagraphSpec.destinationStartPosition,
1221                 styleName: mergeParagraphSpec.paragraphStyleName
1222             });
1223             opSpecsA.push(styleSplitParagraph);
1224             if (splitParagraphSpec.position === mergeParagraphSpec.sourceStartPosition - 1
1225                     && mergeParagraphSpec.moveCursor) {
1226                 // OdtDocument.getTextNodeAtStep + Spec.moveCursor make it very difficult to control cursor placement
1227                 // When a split + merge combines, there is a tricky situation because the split will leave other cursors
1228                 // on the last step in the new paragraph.
1229                 // When the merge is relocated to attach to the front of the newly inserted paragraph below, the cursor
1230                 // will end up at the start of the new paragraph. Workaround this by manually setting the cursor back
1231                 // to the appropriate location after the merge completes
1232                 moveCursorOp = /**@type{!ops.OpMoveCursor.Spec}*/({
1233                     optype: "MoveCursor",
1234                     memberid: mergeParagraphSpec.memberid,
1235                     timestamp: mergeParagraphSpec.timestamp,
1236                     position: splitParagraphSpec.position,
1237                     length: 0
1238                 });
1239                 opSpecsA.push(moveCursorOp);
1240             }
1241 
1242             // SplitParagraph ops effectively create new paragraph boundaries. The user intent
1243             // is for the source paragraph to be joined to the END of the dest paragraph. If the
1244             // split occurs in the dest paragraph, the source should be joined to the newly created
1245             // paragraph instead
1246             mergeParagraphSpec.destinationStartPosition = splitParagraphSpec.position + 1;
1247             mergeParagraphSpec.sourceStartPosition += 1;
1248         } else if (splitParagraphSpec.position >= mergeParagraphSpec.sourceStartPosition) {
1249             // Split occurs after the merge source
1250             // Merging paragraphs remove one step
1251             splitParagraphSpec.position -= 1;
1252             splitParagraphSpec.sourceParagraphPosition -= 1;
1253         }
1254 
1255         return {
1256             opSpecsA:  opSpecsA,
1257             opSpecsB:  opSpecsB
1258         };
1259     }
1260 
1261     /**
1262      * @param {!ops.OpUpdateParagraphStyle.Spec} updateParagraphStyleSpecA
1263      * @param {!ops.OpUpdateParagraphStyle.Spec} updateParagraphStyleSpecB
1264      * @param {!boolean} hasAPriority
1265      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1266      */
1267     function transformUpdateParagraphStyleUpdateParagraphStyle(updateParagraphStyleSpecA, updateParagraphStyleSpecB, hasAPriority) {
1268         var majorSpec, minorSpec,
1269             updateParagraphStyleSpecAResult = [updateParagraphStyleSpecA],
1270             updateParagraphStyleSpecBResult = [updateParagraphStyleSpecB];
1271 
1272         // same style updated by other op?
1273         if (updateParagraphStyleSpecA.styleName === updateParagraphStyleSpecB.styleName) {
1274             majorSpec = hasAPriority ? updateParagraphStyleSpecA : updateParagraphStyleSpecB;
1275             minorSpec = hasAPriority ? updateParagraphStyleSpecB : updateParagraphStyleSpecA;
1276 
1277             // any properties which are set by other update op need to be dropped
1278             dropOverruledAndUnneededProperties(minorSpec.setProperties,
1279                 minorSpec.removedProperties, majorSpec.setProperties,
1280                 majorSpec.removedProperties, 'style:paragraph-properties');
1281             dropOverruledAndUnneededProperties(minorSpec.setProperties,
1282                 minorSpec.removedProperties, majorSpec.setProperties,
1283                 majorSpec.removedProperties, 'style:text-properties');
1284             dropOverruledAndUnneededAttributes(minorSpec.setProperties || null,
1285                 /**@type{{attributes: string}}*/(minorSpec.removedProperties) || null,
1286                 majorSpec.setProperties || null,
1287                 /**@type{{attributes: string}}*/(majorSpec.removedProperties) || null);
1288 
1289             // check if there are any changes left and the major op has not become a noop
1290             if (!(majorSpec.setProperties && hasProperties(majorSpec.setProperties)) &&
1291                     !(majorSpec.removedProperties && hasRemovedProperties(majorSpec.removedProperties))) {
1292                 // set major spec to noop
1293                 if (hasAPriority) {
1294                     updateParagraphStyleSpecAResult = [];
1295                 } else {
1296                     updateParagraphStyleSpecBResult = [];
1297                 }
1298             }
1299             // check if there are any changes left and the minor op has not become a noop
1300             if (!(minorSpec.setProperties && hasProperties(minorSpec.setProperties)) &&
1301                     !(minorSpec.removedProperties && hasRemovedProperties(minorSpec.removedProperties))) {
1302                 // set minor spec to noop 
1303                 if (hasAPriority) {
1304                     updateParagraphStyleSpecBResult = [];
1305                 } else {
1306                     updateParagraphStyleSpecAResult = [];
1307                 }
1308             }
1309         }
1310 
1311         return {
1312             opSpecsA:  updateParagraphStyleSpecAResult,
1313             opSpecsB:  updateParagraphStyleSpecBResult
1314         };
1315     }
1316 
1317     /**
1318      * @param {!ops.OpUpdateMetadata.Spec} updateMetadataSpecA
1319      * @param {!ops.OpUpdateMetadata.Spec} updateMetadataSpecB
1320      * @param {!boolean} hasAPriority
1321      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1322      */
1323     function transformUpdateMetadataUpdateMetadata(updateMetadataSpecA, updateMetadataSpecB, hasAPriority) {
1324         var majorSpec, minorSpec,
1325             updateMetadataSpecAResult = [updateMetadataSpecA],
1326             updateMetadataSpecBResult = [updateMetadataSpecB];
1327 
1328         majorSpec = hasAPriority ? updateMetadataSpecA : updateMetadataSpecB;
1329         minorSpec = hasAPriority ? updateMetadataSpecB : updateMetadataSpecA;
1330 
1331         // any properties which are set by other update op need to be dropped
1332         dropOverruledAndUnneededAttributes(minorSpec.setProperties || null,
1333                             minorSpec.removedProperties || null,
1334                             majorSpec.setProperties || null,
1335                             majorSpec.removedProperties || null);
1336 
1337         // check if there are any changes left and the major op has not become a noop
1338         if (!(majorSpec.setProperties && hasProperties(majorSpec.setProperties)) &&
1339                 !(majorSpec.removedProperties && hasRemovedProperties(majorSpec.removedProperties))) {
1340             // set major spec to noop
1341             if (hasAPriority) {
1342                 updateMetadataSpecAResult = [];
1343             } else {
1344                 updateMetadataSpecBResult = [];
1345             }
1346         }
1347         // check if there are any changes left and the minor op has not become a noop
1348         if (!(minorSpec.setProperties && hasProperties(minorSpec.setProperties)) &&
1349                 !(minorSpec.removedProperties && hasRemovedProperties(minorSpec.removedProperties))) {
1350             // set minor spec to noop 
1351             if (hasAPriority) {
1352                 updateMetadataSpecBResult = [];
1353             } else {
1354                 updateMetadataSpecAResult = [];
1355             }
1356         }
1357 
1358         return {
1359             opSpecsA:  updateMetadataSpecAResult,
1360             opSpecsB:  updateMetadataSpecBResult
1361         };
1362     }
1363 
1364     /**
1365      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpecA
1366      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpecB
1367      * @param {!boolean} hasAPriority
1368      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1369      */
1370     function transformSetParagraphStyleSetParagraphStyle(setParagraphStyleSpecA, setParagraphStyleSpecB, hasAPriority) {
1371         if (setParagraphStyleSpecA.position === setParagraphStyleSpecB.position) {
1372             if (hasAPriority) {
1373                 setParagraphStyleSpecB.styleName = setParagraphStyleSpecA.styleName;
1374             } else {
1375                 setParagraphStyleSpecA.styleName = setParagraphStyleSpecB.styleName;
1376             }
1377         }
1378 
1379         return {
1380             opSpecsA:  [setParagraphStyleSpecA],
1381             opSpecsB:  [setParagraphStyleSpecB]
1382         };
1383     }
1384 
1385     /**
1386      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
1387      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
1388      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1389      */
1390     function transformSetParagraphStyleSplitParagraph(setParagraphStyleSpec, splitParagraphSpec) {
1391         var opSpecsA = [setParagraphStyleSpec],
1392             opSpecsB = [splitParagraphSpec],
1393             setParagraphClone;
1394 
1395         if (setParagraphStyleSpec.position > splitParagraphSpec.position) {
1396             setParagraphStyleSpec.position += 1;
1397         } else if (setParagraphStyleSpec.position === splitParagraphSpec.sourceParagraphPosition) {
1398             // When a set paragraph style & split conflict, the set paragraph style always wins
1399 
1400             splitParagraphSpec.paragraphStyleName = setParagraphStyleSpec.styleName;
1401             // The new paragraph that resulted from the already executed split op should be styled with
1402             // the original paragraph style.
1403             setParagraphClone = cloneOpspec(setParagraphStyleSpec);
1404             // A split paragraph op introduces a new paragraph boundary just passed the point where the split occurs
1405             setParagraphClone.position = splitParagraphSpec.position + 1;
1406             opSpecsA.push(setParagraphClone);
1407         }
1408 
1409         return {
1410             opSpecsA:  opSpecsA,
1411             opSpecsB:  opSpecsB
1412         };
1413     }
1414 
1415     /**
1416      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpecA
1417      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpecB
1418      * @param {!boolean} hasAPriority
1419      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1420      */
1421     function transformSplitParagraphSplitParagraph(splitParagraphSpecA, splitParagraphSpecB, hasAPriority) {
1422         var specABeforeB,
1423             specBBeforeA;
1424 
1425         if (splitParagraphSpecA.position < splitParagraphSpecB.position) {
1426             specABeforeB =  true;
1427         } else if (splitParagraphSpecB.position < splitParagraphSpecA.position) {
1428             specBBeforeA = true;
1429         } else if (splitParagraphSpecA.position === splitParagraphSpecB.position) {
1430             if (hasAPriority) {
1431                 specABeforeB =  true;
1432             } else {
1433                 specBBeforeA = true;
1434             }
1435         }
1436 
1437         if (specABeforeB) {
1438             splitParagraphSpecB.position += 1;
1439             if (splitParagraphSpecA.position < splitParagraphSpecB.sourceParagraphPosition) {
1440                 splitParagraphSpecB.sourceParagraphPosition += 1;
1441             } else {
1442                 // Split occurs between specB's split position & it's source paragraph position
1443                 // This means specA introduces a NEW paragraph boundary
1444                 splitParagraphSpecB.sourceParagraphPosition = splitParagraphSpecA.position + 1;
1445             }
1446         } else if (specBBeforeA) {
1447             splitParagraphSpecA.position += 1;
1448             if (splitParagraphSpecB.position < splitParagraphSpecB.sourceParagraphPosition) {
1449                 splitParagraphSpecA.sourceParagraphPosition += 1;
1450             } else {
1451                 // Split occurs between specA's split position & it's source paragraph position
1452                 // This means specB introduces a NEW paragraph boundary
1453                 splitParagraphSpecA.sourceParagraphPosition = splitParagraphSpecB.position + 1;
1454             }
1455         }
1456 
1457         return {
1458             opSpecsA:  [splitParagraphSpecA],
1459             opSpecsB:  [splitParagraphSpecB]
1460         };
1461     }
1462 
1463     /**
1464      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
1465      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
1466      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1467      */
1468     function transformMoveCursorRemoveAnnotation(moveCursorSpec, removeAnnotationSpec) {
1469         var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec),
1470             moveCursorSpecEnd = moveCursorSpec.position + moveCursorSpec.length,
1471             removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length;
1472 
1473         // check if inside removed annotation
1474         if (removeAnnotationSpec.position <= moveCursorSpec.position && moveCursorSpecEnd <= removeAnnotationEnd) {
1475             moveCursorSpec.position = removeAnnotationSpec.position - 1;
1476             moveCursorSpec.length = 0;
1477         } else {
1478             if (removeAnnotationEnd < moveCursorSpec.position) {
1479                 moveCursorSpec.position -= removeAnnotationSpec.length + 2;
1480             } else if (removeAnnotationEnd < moveCursorSpecEnd) {
1481                 moveCursorSpec.length -= removeAnnotationSpec.length + 2;
1482             }
1483             if (isMoveCursorSpecRangeInverted) {
1484                 invertMoveCursorSpecRange(moveCursorSpec);
1485             }
1486         }
1487 
1488         return {
1489             opSpecsA:  [moveCursorSpec],
1490             opSpecsB:  [removeAnnotationSpec]
1491         };
1492     }
1493 
1494     /**
1495      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
1496      * @param {!ops.OpRemoveCursor.Spec} removeCursorSpec
1497      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1498      */
1499     function transformMoveCursorRemoveCursor(moveCursorSpec, removeCursorSpec) {
1500         var isSameCursorRemoved = (moveCursorSpec.memberid === removeCursorSpec.memberid);
1501 
1502         return {
1503             opSpecsA:  isSameCursorRemoved ? [] : [moveCursorSpec],
1504             opSpecsB:  [removeCursorSpec]
1505         };
1506     }
1507 
1508     /**
1509      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
1510      * @param {!ops.OpRemoveText.Spec} removeTextSpec
1511      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1512      */
1513     function transformMoveCursorRemoveText(moveCursorSpec, removeTextSpec) {
1514         var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec),
1515             moveCursorSpecEnd = moveCursorSpec.position + moveCursorSpec.length,
1516             removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length;
1517 
1518         // transform moveCursorSpec
1519         // removed positions by object up to move cursor position?
1520         if (removeTextSpecEnd <= moveCursorSpec.position) {
1521             // adapt by removed position
1522             moveCursorSpec.position -= removeTextSpec.length;
1523         // overlapping?
1524         } else if (removeTextSpec.position < moveCursorSpecEnd) {
1525             // still to select range starting at cursor position?
1526             if (moveCursorSpec.position < removeTextSpec.position) {
1527                 // still to select range ending at selection?
1528                 if (removeTextSpecEnd < moveCursorSpecEnd) {
1529                     moveCursorSpec.length -= removeTextSpec.length;
1530                 } else {
1531                     moveCursorSpec.length = removeTextSpec.position - moveCursorSpec.position;
1532                 }
1533             // remove overlapping section
1534             } else {
1535                 // fall at start of removed section
1536                 moveCursorSpec.position = removeTextSpec.position;
1537                 // still to select range at selection end?
1538                 if (removeTextSpecEnd < moveCursorSpecEnd) {
1539                     moveCursorSpec.length = moveCursorSpecEnd - removeTextSpecEnd;
1540                 } else {
1541                     // completely overlapped by other, so selection gets void
1542                     moveCursorSpec.length = 0;
1543                 }
1544             }
1545         }
1546 
1547         if (isMoveCursorSpecRangeInverted) {
1548             invertMoveCursorSpecRange(moveCursorSpec);
1549         }
1550 
1551         return {
1552             opSpecsA:  [moveCursorSpec],
1553             opSpecsB:  [removeTextSpec]
1554         };
1555     }
1556 
1557     /**
1558      * @param {!ops.OpMoveCursor.Spec} moveCursorSpec
1559      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
1560      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1561      */
1562     function transformMoveCursorSplitParagraph(moveCursorSpec, splitParagraphSpec) {
1563         var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec);
1564 
1565         // transform moveCursorSpec
1566         if (splitParagraphSpec.position < moveCursorSpec.position) {
1567             moveCursorSpec.position += 1;
1568         } else if (splitParagraphSpec.position < moveCursorSpec.position + moveCursorSpec.length) {
1569             moveCursorSpec.length += 1;
1570         }
1571 
1572         if (isMoveCursorSpecRangeInverted) {
1573             invertMoveCursorSpecRange(moveCursorSpec);
1574         }
1575 
1576         return {
1577             opSpecsA:  [moveCursorSpec],
1578             opSpecsB:  [splitParagraphSpec]
1579         };
1580     }
1581 
1582     /**
1583      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpecA
1584      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpecB
1585      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1586      */
1587     function transformRemoveAnnotationRemoveAnnotation(removeAnnotationSpecA, removeAnnotationSpecB) {
1588         var removeAnnotationSpecAResult = [removeAnnotationSpecA],
1589             removeAnnotationSpecBResult = [removeAnnotationSpecB];
1590 
1591         // check if removing the same annotation
1592         if (removeAnnotationSpecA.position === removeAnnotationSpecB.position && removeAnnotationSpecA.length === removeAnnotationSpecB.length) {
1593             removeAnnotationSpecAResult = [];
1594             removeAnnotationSpecBResult = [];
1595         } else {
1596             if (removeAnnotationSpecA.position < removeAnnotationSpecB.position) {
1597                 removeAnnotationSpecB.position -= removeAnnotationSpecA.length + 2;
1598             } else {
1599                 removeAnnotationSpecA.position -= removeAnnotationSpecB.length + 2;
1600             }
1601         }
1602 
1603         return {
1604             opSpecsA:  removeAnnotationSpecAResult,
1605             opSpecsB:  removeAnnotationSpecBResult
1606         };
1607     }
1608 
1609     /**
1610      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
1611      * @param {!ops.OpRemoveText.Spec} removeTextSpec
1612      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1613      */
1614     function transformRemoveAnnotationRemoveText(removeAnnotationSpec, removeTextSpec) {
1615         var removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length,
1616             removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length,
1617             removeAnnotationSpecResult = [removeAnnotationSpec],
1618             removeTextSpecResult = [removeTextSpec];
1619 
1620         // check if inside removed annotation
1621         if (removeAnnotationSpec.position <= removeTextSpec.position && removeTextSpecEnd <= removeAnnotationEnd) {
1622             removeTextSpecResult = [];
1623             removeAnnotationSpec.length -= removeTextSpec.length;
1624         } else {
1625             if (removeTextSpecEnd < removeAnnotationSpec.position) {
1626                 removeAnnotationSpec.position -= removeTextSpec.length;
1627             } else if (removeTextSpec.position < removeAnnotationSpec.position) {
1628                 removeAnnotationSpec.position = removeTextSpec.position + 1;
1629                 removeTextSpec.length -= removeAnnotationSpec.length + 2;
1630             } else {
1631                 removeTextSpec.position -= removeAnnotationSpec.length + 2;
1632             }
1633         }
1634 
1635         return {
1636             opSpecsA:  removeAnnotationSpecResult,
1637             opSpecsB:  removeTextSpecResult
1638         };
1639     }
1640 
1641     /**
1642      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
1643      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
1644      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1645      */
1646     function transformRemoveAnnotationSetParagraphStyle(removeAnnotationSpec, setParagraphStyleSpec) {
1647         var setParagraphStyleSpecPosition = setParagraphStyleSpec.position,
1648             removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length,
1649             removeAnnotationSpecResult = [removeAnnotationSpec],
1650             setParagraphStyleSpecResult = [setParagraphStyleSpec];
1651 
1652         // check if inside removed annotation
1653         if (removeAnnotationSpec.position <= setParagraphStyleSpecPosition && setParagraphStyleSpecPosition <= removeAnnotationEnd) {
1654             setParagraphStyleSpecResult = [];
1655         } else {
1656             if (removeAnnotationEnd < setParagraphStyleSpecPosition) {
1657                 setParagraphStyleSpec.position -= removeAnnotationSpec.length + 2;
1658             }
1659         }
1660 
1661         return {
1662             opSpecsA:  removeAnnotationSpecResult,
1663             opSpecsB:  setParagraphStyleSpecResult
1664         };
1665     }
1666 
1667     /**
1668      * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec
1669      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
1670      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1671      */
1672     function transformRemoveAnnotationSplitParagraph(removeAnnotationSpec, splitParagraphSpec) {
1673         var splitParagraphSpecPosition = splitParagraphSpec.position,
1674             removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length,
1675             removeAnnotationSpecResult = [removeAnnotationSpec],
1676             splitParagraphSpecResult = [splitParagraphSpec];
1677 
1678         // check if inside removed annotation
1679         if (removeAnnotationSpec.position <= splitParagraphSpecPosition && splitParagraphSpecPosition <= removeAnnotationEnd) {
1680             splitParagraphSpecResult = [];
1681             removeAnnotationSpec.length += 1;
1682         } else {
1683             if (removeAnnotationEnd < splitParagraphSpec.sourceParagraphPosition) {
1684                 splitParagraphSpec.sourceParagraphPosition -= removeAnnotationSpec.length + 2;
1685             }
1686             if (removeAnnotationEnd < splitParagraphSpecPosition) {
1687                 splitParagraphSpec.position -= removeAnnotationSpec.length + 2;
1688             } else {
1689                 removeAnnotationSpec.position += 1;
1690             }
1691         }
1692 
1693         return {
1694             opSpecsA:  removeAnnotationSpecResult,
1695             opSpecsB:  splitParagraphSpecResult
1696         };
1697     }
1698 
1699     /**
1700      * @param {!ops.OpRemoveCursor.Spec} removeCursorSpecA
1701      * @param {!ops.OpRemoveCursor.Spec} removeCursorSpecB
1702      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1703      */
1704     function transformRemoveCursorRemoveCursor(removeCursorSpecA, removeCursorSpecB) {
1705         var isSameMemberid = (removeCursorSpecA.memberid === removeCursorSpecB.memberid);
1706 
1707         // if both are removing the same cursor, their transformed counter-ops become noops
1708         return {
1709             opSpecsA:  isSameMemberid ? [] : [removeCursorSpecA],
1710             opSpecsB:  isSameMemberid ? [] : [removeCursorSpecB]
1711         };
1712     }
1713 
1714     /**
1715      * @param {!ops.OpRemoveStyle.Spec} removeStyleSpecA
1716      * @param {!ops.OpRemoveStyle.Spec} removeStyleSpecB
1717      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1718      */
1719     function transformRemoveStyleRemoveStyle(removeStyleSpecA, removeStyleSpecB) {
1720         var isSameStyle = (removeStyleSpecA.styleName === removeStyleSpecB.styleName && removeStyleSpecA.styleFamily === removeStyleSpecB.styleFamily);
1721 
1722         // if both are removing the same style, their transformed counter-ops become noops
1723         return {
1724             opSpecsA:  isSameStyle ? [] : [removeStyleSpecA],
1725             opSpecsB:  isSameStyle ? [] : [removeStyleSpecB]
1726         };
1727     }
1728 
1729     /**
1730      * @param {!ops.OpRemoveStyle.Spec} removeStyleSpec
1731      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
1732      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1733      */
1734     function transformRemoveStyleSetParagraphStyle(removeStyleSpec, setParagraphStyleSpec) {
1735         var helperOpspec,
1736             removeStyleSpecResult = [removeStyleSpec],
1737             setParagraphStyleSpecResult = [setParagraphStyleSpec];
1738 
1739         if (removeStyleSpec.styleFamily === "paragraph" && removeStyleSpec.styleName === setParagraphStyleSpec.styleName) {
1740             // transform removeStyleSpec
1741             // just create a setstyle op preceding to us which removes any set style from the paragraph
1742             helperOpspec = {
1743                 optype: "SetParagraphStyle",
1744                 memberid: removeStyleSpec.memberid,
1745                 timestamp: removeStyleSpec.timestamp,
1746                 position: setParagraphStyleSpec.position,
1747                 styleName: ""
1748             };
1749             removeStyleSpecResult.unshift(helperOpspec);
1750 
1751             // transform setParagraphStyleSpec
1752             // instead of setting now remove any existing style from the paragraph
1753             setParagraphStyleSpec.styleName = "";
1754         }
1755 
1756         return {
1757             opSpecsA:  removeStyleSpecResult,
1758             opSpecsB:  setParagraphStyleSpecResult
1759         };
1760     }
1761 
1762     /**
1763      * @param {!ops.OpRemoveStyle.Spec} removeStyleSpec
1764      * @param {!ops.OpUpdateParagraphStyle.Spec} updateParagraphStyleSpec
1765      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1766      */
1767     function transformRemoveStyleUpdateParagraphStyle(removeStyleSpec, updateParagraphStyleSpec) {
1768         var setAttributes, helperOpspec,
1769             removeStyleSpecResult = [removeStyleSpec],
1770             updateParagraphStyleSpecResult = [updateParagraphStyleSpec];
1771 
1772         if (removeStyleSpec.styleFamily === "paragraph") {
1773             // transform removeStyleSpec
1774             // style brought into use by other op?
1775             setAttributes = getStyleReferencingAttributes(updateParagraphStyleSpec.setProperties, removeStyleSpec.styleName);
1776             if (setAttributes.length > 0) {
1777                 // just create a updateparagraph style op preceding to us which removes any set style from the paragraph
1778                 helperOpspec = {
1779                     optype: "UpdateParagraphStyle",
1780                     memberid: removeStyleSpec.memberid,
1781                     timestamp: removeStyleSpec.timestamp,
1782                     styleName: updateParagraphStyleSpec.styleName,
1783                     removedProperties: { attributes: setAttributes.join(',') }
1784                 };
1785                 removeStyleSpecResult.unshift(helperOpspec);
1786             }
1787 
1788             // transform updateParagraphStyleSpec
1789             // target style to update deleted by removeStyle?
1790             if (removeStyleSpec.styleName === updateParagraphStyleSpec.styleName) {
1791                 // don't touch the dead
1792                 updateParagraphStyleSpecResult = [];
1793             } else {
1794                 // otherwise drop any attributes referencing the style deleted
1795                 dropStyleReferencingAttributes(updateParagraphStyleSpec.setProperties, removeStyleSpec.styleName);
1796             }
1797         }
1798 
1799         return {
1800             opSpecsA:  removeStyleSpecResult,
1801             opSpecsB:  updateParagraphStyleSpecResult
1802         };
1803     }
1804 
1805     /**
1806      * @param {!ops.OpRemoveText.Spec} removeTextSpecA
1807      * @param {!ops.OpRemoveText.Spec} removeTextSpecB
1808      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1809      */
1810     function transformRemoveTextRemoveText(removeTextSpecA, removeTextSpecB) {
1811         var removeTextSpecAEnd = removeTextSpecA.position + removeTextSpecA.length,
1812             removeTextSpecBEnd = removeTextSpecB.position + removeTextSpecB.length,
1813             removeTextSpecAResult = [removeTextSpecA],
1814             removeTextSpecBResult = [removeTextSpecB];
1815 
1816         // B removed positions by object up to As start position?
1817         if (removeTextSpecBEnd <= removeTextSpecA.position) {
1818             // adapt A by removed position
1819             removeTextSpecA.position -= removeTextSpecB.length;
1820         // A removed positions by object up to Bs start position?
1821         } else if (removeTextSpecAEnd <= removeTextSpecB.position) {
1822             // adapt B by removed position
1823             removeTextSpecB.position -= removeTextSpecA.length;
1824         // overlapping?
1825         // (removeTextSpecBEnd <= removeTextSpecA.position above catches non-overlapping from this condition)
1826         } else if (removeTextSpecB.position < removeTextSpecAEnd) {
1827             // A removes in front of B?
1828             if (removeTextSpecA.position < removeTextSpecB.position) {
1829                 // A still to remove range at its end?
1830                 if (removeTextSpecBEnd < removeTextSpecAEnd) {
1831                     removeTextSpecA.length = removeTextSpecA.length - removeTextSpecB.length;
1832                 } else {
1833                     removeTextSpecA.length = removeTextSpecB.position - removeTextSpecA.position;
1834                 }
1835                 // B still to remove range at its end?
1836                 if (removeTextSpecAEnd < removeTextSpecBEnd) {
1837                     removeTextSpecB.position = removeTextSpecA.position;
1838                     removeTextSpecB.length = removeTextSpecBEnd - removeTextSpecAEnd;
1839                 } else {
1840                     // B completely overlapped by other, so it becomes a noop
1841                     removeTextSpecBResult = [];
1842                 }
1843             // B removes in front of or starting at same like A
1844             } else {
1845                 // B still to remove range at its end?
1846                 if (removeTextSpecAEnd < removeTextSpecBEnd) {
1847                     removeTextSpecB.length = removeTextSpecB.length - removeTextSpecA.length;
1848                 } else {
1849                     // B still to remove range at its start?
1850                     if (removeTextSpecB.position < removeTextSpecA.position) {
1851                         removeTextSpecB.length = removeTextSpecA.position - removeTextSpecB.position;
1852                     } else {
1853                         // B completely overlapped by other, so it becomes a noop
1854                         removeTextSpecBResult = [];
1855                     }
1856                 }
1857                 // A still to remove range at its end?
1858                 if (removeTextSpecBEnd < removeTextSpecAEnd) {
1859                     removeTextSpecA.position = removeTextSpecB.position;
1860                     removeTextSpecA.length = removeTextSpecAEnd - removeTextSpecBEnd;
1861                 } else {
1862                     // A completely overlapped by other, so it becomes a noop
1863                     removeTextSpecAResult = [];
1864                 }
1865             }
1866         }
1867         return {
1868             opSpecsA:  removeTextSpecAResult,
1869             opSpecsB:  removeTextSpecBResult
1870         };
1871     }
1872 
1873     /**
1874      * @param {!ops.OpRemoveText.Spec} removeTextSpec
1875      * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec
1876      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1877      */
1878     function transformRemoveTextSetParagraphStyle(removeTextSpec, setParagraphStyleSpec) {
1879         // Removal is done entirely in some preceding paragraph
1880         if (removeTextSpec.position < setParagraphStyleSpec.position) {
1881             setParagraphStyleSpec.position -= removeTextSpec.length;
1882         }
1883 
1884         return {
1885             opSpecsA:  [removeTextSpec],
1886             opSpecsB:  [setParagraphStyleSpec]
1887         };
1888     }
1889 
1890     /**
1891      * @param {!ops.OpRemoveText.Spec} removeTextSpec
1892      * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec
1893      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1894      */
1895     function transformRemoveTextSplitParagraph(removeTextSpec, splitParagraphSpec) {
1896         var removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length,
1897             helperOpspec,
1898             removeTextSpecResult = [removeTextSpec],
1899             splitParagraphSpecResult = [splitParagraphSpec];
1900 
1901         // adapt removeTextSpec
1902         if (splitParagraphSpec.position <= removeTextSpec.position) {
1903             removeTextSpec.position += 1;
1904         } else if (splitParagraphSpec.position < removeTextSpecEnd) {
1905             // we have to split the removal into two ops, before and after the insertion
1906             removeTextSpec.length = splitParagraphSpec.position - removeTextSpec.position;
1907             helperOpspec = {
1908                 optype: "RemoveText",
1909                 memberid: removeTextSpec.memberid,
1910                 timestamp: removeTextSpec.timestamp,
1911                 position: splitParagraphSpec.position + 1,
1912                 length: removeTextSpecEnd - splitParagraphSpec.position
1913             };
1914             removeTextSpecResult.unshift(helperOpspec); // helperOp first, so its position is not affected by the real op
1915         }
1916 
1917         // adapt splitParagraphSpec
1918         if (removeTextSpec.position + removeTextSpec.length <= splitParagraphSpec.position) {
1919             splitParagraphSpec.position -= removeTextSpec.length;
1920         } else if (removeTextSpec.position < splitParagraphSpec.position) {
1921             splitParagraphSpec.position = removeTextSpec.position;
1922         }
1923 
1924         if (removeTextSpec.position + removeTextSpec.length < splitParagraphSpec.sourceParagraphPosition) {
1925             // Removed text is before the source paragraph
1926             splitParagraphSpec.sourceParagraphPosition -= removeTextSpec.length;
1927         }
1928         // removeText ops can't cross over paragraph boundaries, so don't check this case
1929 
1930         return {
1931             opSpecsA:  removeTextSpecResult,
1932             opSpecsB:  splitParagraphSpecResult
1933         };
1934     }
1935 
1936     /**
1937      * Does an OT on the two passed opspecs, where they are not modified at all,
1938      * and so simply returns them in the result arrays.
1939      * @param {!Object} opSpecA
1940      * @param {!Object} opSpecB
1941      * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1942      */
1943     function passUnchanged(opSpecA, opSpecB) {
1944         return {
1945             opSpecsA:  [opSpecA],
1946             opSpecsB:  [opSpecB]
1947         };
1948     }
1949 
1950 
1951     var /**
1952          * This is the lower-left half of the sparse NxN matrix with all the
1953          * transformation methods on the possible pairs of ops. As the matrix
1954          * is symmetric, only that half is used. So the user of this matrix has
1955          * to ensure the proper order of opspecs on lookup and on calling the
1956          * picked transformation method.
1957          *
1958          * Each transformation method takes the two opspecs (and optionally
1959          * a flag if the first has a higher priority, in case of tie breaking
1960          * having to be done). The method returns a record with the two
1961          * resulting arrays of ops, with key names "opSpecsA" and "opSpecsB".
1962          * Those arrays could have more than the initial respective opspec
1963          * inside, in case some additional helper opspecs are needed, or be
1964          * empty if the opspec turned into a no-op in the transformation.
1965          * If a transformation is not doable, the method returns "null".
1966          *
1967          * Some operations are added onto the stack only by the master session,
1968          * for example AddMember, RemoveMember, and UpdateMember. These therefore need
1969          * not be transformed against each other, since the master session is the
1970          * only originator of these ops. Therefore, their pairing entries in the
1971          * matrix are missing. They do however require a passUnchanged entry
1972          * with the other ops.
1973          *
1974          * Here the CC signature of each transformation method:
1975          * param {!Object} opspecA
1976          * param {!Object} opspecB
1977          * (param {!boolean} hasAPriorityOverB)  can be left out
1978          * return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}}
1979          *
1980          * Empty cells in this matrix mean there is no such transformation
1981          * possible, and should be handled as if the method returns "null".
1982          *
1983          * @type {!Object.<string,!Object.<string,function(!Object,!Object,boolean=):?{opSpecsA:!Array.<!{optype:string}>, opSpecsB:!Array.<!{optype:string}>}>>}
1984          */
1985         transformations;
1986     transformations = {
1987         "AddAnnotation": {
1988             "AddAnnotation":        transformAddAnnotationAddAnnotation,
1989             "AddCursor":            passUnchanged,
1990             "AddMember":            passUnchanged,
1991             "AddStyle":             passUnchanged,
1992             "ApplyDirectStyling":   transformAddAnnotationApplyDirectStyling,
1993             "InsertText":           transformAddAnnotationInsertText,
1994             "MergeParagraph":       transformAddAnnotationMergeParagraph,
1995             "MoveCursor":           transformAddAnnotationMoveCursor,
1996             "RemoveAnnotation":     transformAddAnnotationRemoveAnnotation,
1997             "RemoveCursor":         passUnchanged,
1998             "RemoveMember":         passUnchanged,
1999             "RemoveStyle":          passUnchanged,
2000             "RemoveText":           transformAddAnnotationRemoveText,
2001             "SetParagraphStyle":    transformAddAnnotationSetParagraphStyle,
2002             "SplitParagraph":       transformAddAnnotationSplitParagraph,
2003             "UpdateMember":         passUnchanged,
2004             "UpdateMetadata":       passUnchanged,
2005             "UpdateParagraphStyle": passUnchanged
2006         },
2007         "AddCursor": {
2008             "AddCursor":            passUnchanged,
2009             "AddMember":            passUnchanged,
2010             "AddStyle":             passUnchanged,
2011             "ApplyDirectStyling":   passUnchanged,
2012             "InsertText":           passUnchanged,
2013             "MergeParagraph":       passUnchanged,
2014             "MoveCursor":           passUnchanged,
2015             "RemoveAnnotation":     passUnchanged,
2016             "RemoveCursor":         passUnchanged,
2017             "RemoveMember":         passUnchanged,
2018             "RemoveStyle":          passUnchanged,
2019             "RemoveText":           passUnchanged,
2020             "SetParagraphStyle":    passUnchanged,
2021             "SplitParagraph":       passUnchanged,
2022             "UpdateMember":         passUnchanged,
2023             "UpdateMetadata":       passUnchanged,
2024             "UpdateParagraphStyle": passUnchanged
2025         },
2026         "AddMember": {
2027             "AddStyle":             passUnchanged,
2028             "ApplyDirectStyling":   passUnchanged,
2029             "InsertText":           passUnchanged,
2030             "MergeParagraph":       passUnchanged,
2031             "MoveCursor":           passUnchanged,
2032             "RemoveAnnotation":     passUnchanged,
2033             "RemoveCursor":         passUnchanged,
2034             "RemoveStyle":          passUnchanged,
2035             "RemoveText":           passUnchanged,
2036             "SetParagraphStyle":    passUnchanged,
2037             "SplitParagraph":       passUnchanged,
2038             "UpdateMetadata":       passUnchanged,
2039             "UpdateParagraphStyle": passUnchanged
2040         },
2041         "AddStyle": {
2042             "AddStyle":             passUnchanged,
2043             "ApplyDirectStyling":   passUnchanged,
2044             "InsertText":           passUnchanged,
2045             "MergeParagraph":       passUnchanged,
2046             "MoveCursor":           passUnchanged,
2047             "RemoveAnnotation":     passUnchanged,
2048             "RemoveCursor":         passUnchanged,
2049             "RemoveMember":         passUnchanged,
2050             "RemoveStyle":          transformAddStyleRemoveStyle,
2051             "RemoveText":           passUnchanged,
2052             "SetParagraphStyle":    passUnchanged,
2053             "SplitParagraph":       passUnchanged,
2054             "UpdateMember":         passUnchanged,
2055             "UpdateMetadata":       passUnchanged,
2056             "UpdateParagraphStyle": passUnchanged
2057         },
2058         "ApplyDirectStyling": {
2059             "ApplyDirectStyling":   transformApplyDirectStylingApplyDirectStyling,
2060             "InsertText":           transformApplyDirectStylingInsertText,
2061             "MergeParagraph":       transformApplyDirectStylingMergeParagraph,
2062             "MoveCursor":           passUnchanged,
2063             "RemoveAnnotation":     transformApplyDirectStylingRemoveAnnotation,
2064             "RemoveCursor":         passUnchanged,
2065             "RemoveMember":         passUnchanged,
2066             "RemoveStyle":          passUnchanged,
2067             "RemoveText":           transformApplyDirectStylingRemoveText,
2068             "SetParagraphStyle":    passUnchanged,
2069             "SplitParagraph":       transformApplyDirectStylingSplitParagraph,
2070             "UpdateMember":         passUnchanged,
2071             "UpdateMetadata":       passUnchanged,
2072             "UpdateParagraphStyle": passUnchanged
2073         },
2074         "InsertText": {
2075             "InsertText":           transformInsertTextInsertText,
2076             "MergeParagraph":       transformInsertTextMergeParagraph,
2077             "MoveCursor":           transformInsertTextMoveCursor,
2078             "RemoveAnnotation":     transformInsertTextRemoveAnnotation,
2079             "RemoveCursor":         passUnchanged,
2080             "RemoveMember":         passUnchanged,
2081             "RemoveStyle":          passUnchanged,
2082             "RemoveText":           transformInsertTextRemoveText,
2083             "SetParagraphStyle":    transformInsertTextSetParagraphStyle,
2084             "SplitParagraph":       transformInsertTextSplitParagraph,
2085             "UpdateMember":         passUnchanged,
2086             "UpdateMetadata":       passUnchanged,
2087             "UpdateParagraphStyle": passUnchanged
2088         },
2089         "MergeParagraph": {
2090             "MergeParagraph":       transformMergeParagraphMergeParagraph,
2091             "MoveCursor":           transformMergeParagraphMoveCursor,
2092             "RemoveAnnotation":     transformMergeParagraphRemoveAnnotation,
2093             "RemoveCursor":         passUnchanged,
2094             "RemoveMember":         passUnchanged,
2095             "RemoveStyle":          passUnchanged,
2096             "RemoveText":           transformMergeParagraphRemoveText,
2097             "SetParagraphStyle":    transformMergeParagraphSetParagraphStyle,
2098             "SplitParagraph":       transformMergeParagraphSplitParagraph,
2099             "UpdateMember":         passUnchanged,
2100             "UpdateMetadata":       passUnchanged,
2101             "UpdateParagraphStyle": passUnchanged
2102         },
2103         "MoveCursor": {
2104             "MoveCursor":           passUnchanged,
2105             "RemoveAnnotation":     transformMoveCursorRemoveAnnotation,
2106             "RemoveCursor":         transformMoveCursorRemoveCursor,
2107             "RemoveMember":         passUnchanged,
2108             "RemoveStyle":          passUnchanged,
2109             "RemoveText":           transformMoveCursorRemoveText,
2110             "SetParagraphStyle":    passUnchanged,
2111             "SplitParagraph":       transformMoveCursorSplitParagraph,
2112             "UpdateMember":         passUnchanged,
2113             "UpdateMetadata":       passUnchanged,
2114             "UpdateParagraphStyle": passUnchanged
2115         },
2116         "RemoveAnnotation": {
2117             "RemoveAnnotation":     transformRemoveAnnotationRemoveAnnotation,
2118             "RemoveCursor":         passUnchanged,
2119             "RemoveMember":         passUnchanged,
2120             "RemoveStyle":          passUnchanged,
2121             "RemoveText":           transformRemoveAnnotationRemoveText,
2122             "SetParagraphStyle":    transformRemoveAnnotationSetParagraphStyle,
2123             "SplitParagraph":       transformRemoveAnnotationSplitParagraph,
2124             "UpdateMember":         passUnchanged,
2125             "UpdateMetadata":       passUnchanged,
2126             "UpdateParagraphStyle": passUnchanged
2127         },
2128         "RemoveCursor": {
2129             "RemoveCursor":         transformRemoveCursorRemoveCursor,
2130             "RemoveMember":         passUnchanged,
2131             "RemoveStyle":          passUnchanged,
2132             "RemoveText":           passUnchanged,
2133             "SetParagraphStyle":    passUnchanged,
2134             "SplitParagraph":       passUnchanged,
2135             "UpdateMember":         passUnchanged,
2136             "UpdateMetadata":       passUnchanged,
2137             "UpdateParagraphStyle": passUnchanged
2138         },
2139         "RemoveMember": {
2140             "RemoveStyle":          passUnchanged,
2141             "RemoveText":           passUnchanged,
2142             "SetParagraphStyle":    passUnchanged,
2143             "SplitParagraph":       passUnchanged,
2144             "UpdateMetadata":       passUnchanged,
2145             "UpdateParagraphStyle": passUnchanged
2146         },
2147         "RemoveStyle": {
2148             "RemoveStyle":          transformRemoveStyleRemoveStyle,
2149             "RemoveText":           passUnchanged,
2150             "SetParagraphStyle":    transformRemoveStyleSetParagraphStyle,
2151             "SplitParagraph":       passUnchanged,
2152             "UpdateMember":         passUnchanged,
2153             "UpdateMetadata":       passUnchanged,
2154             "UpdateParagraphStyle": transformRemoveStyleUpdateParagraphStyle
2155         },
2156         "RemoveText": {
2157             "RemoveText":           transformRemoveTextRemoveText,
2158             "SetParagraphStyle":    transformRemoveTextSetParagraphStyle,
2159             "SplitParagraph":       transformRemoveTextSplitParagraph,
2160             "UpdateMember":         passUnchanged,
2161             "UpdateMetadata":       passUnchanged,
2162             "UpdateParagraphStyle": passUnchanged
2163         },
2164         "SetParagraphStyle": {
2165             "SetParagraphStyle":    transformSetParagraphStyleSetParagraphStyle,
2166             "SplitParagraph":       transformSetParagraphStyleSplitParagraph,
2167             "UpdateMember":         passUnchanged,
2168             "UpdateMetadata":       passUnchanged,
2169             "UpdateParagraphStyle": passUnchanged
2170         },
2171         "SplitParagraph": {
2172             "SplitParagraph":       transformSplitParagraphSplitParagraph,
2173             "UpdateMember":         passUnchanged,
2174             "UpdateMetadata":       passUnchanged,
2175             "UpdateParagraphStyle": passUnchanged
2176         },
2177         "UpdateMember": {
2178             "UpdateMetadata":       passUnchanged,
2179             "UpdateParagraphStyle": passUnchanged
2180         },
2181         "UpdateMetadata": {
2182             "UpdateMetadata":       transformUpdateMetadataUpdateMetadata,
2183             "UpdateParagraphStyle": passUnchanged
2184         },
2185         "UpdateParagraphStyle": {
2186             "UpdateParagraphStyle": transformUpdateParagraphStyleUpdateParagraphStyle
2187         }
2188     };
2189 
2190     this.passUnchanged = passUnchanged;
2191 
2192     /**
2193      * @param {!Object.<!string,!Object.<!string,!Function>>}  moreTransformations
2194      * @return {undefined}
2195      */
2196     this.extendTransformations = function (moreTransformations) {
2197         Object.keys(moreTransformations).forEach(function (optypeA) {
2198             var moreTransformationsOptypeAMap = moreTransformations[optypeA],
2199                 /**@type{!Object.<string,!Function>}*/
2200                 optypeAMap,
2201                 isExtendingOptypeAMap = transformations.hasOwnProperty(optypeA);
2202 
2203             runtime.log((isExtendingOptypeAMap ? "Extending" : "Adding") + " map for optypeA: " + optypeA);
2204             if (!isExtendingOptypeAMap) {
2205                 transformations[optypeA] = {};
2206             }
2207             optypeAMap = transformations[optypeA];
2208 
2209             Object.keys(moreTransformationsOptypeAMap).forEach(function (optypeB) {
2210                 var isOverwritingOptypeBEntry = optypeAMap.hasOwnProperty(optypeB);
2211                 runtime.assert(optypeA <= optypeB, "Wrong order:" + optypeA + ", " + optypeB);
2212                 runtime.log("  " + (isOverwritingOptypeBEntry ? "Overwriting" : "Adding") + " entry for optypeB: " + optypeB);
2213                 optypeAMap[optypeB] = moreTransformationsOptypeAMap[optypeB];
2214             });
2215         });
2216     };
2217 
2218     /**
2219      * @param {!{optype:string}} opSpecA op with lower priority in case of tie breaking
2220      * @param {!{optype:string}} opSpecB op with higher priority in case of tie breaking
2221      * @return {?{opSpecsA:!Array.<!{optype:string}>,
2222      *            opSpecsB:!Array.<!{optype:string}>}}
2223      */
2224     this.transformOpspecVsOpspec = function (opSpecA, opSpecB) {
2225         var isOptypeAAlphaNumericSmaller = (opSpecA.optype <= opSpecB.optype),
2226             helper, transformationFunctionMap, transformationFunction, result;
2227 
2228 runtime.log("Crosstransforming:");
2229 runtime.log(runtime.toJson(opSpecA));
2230 runtime.log(runtime.toJson(opSpecB));
2231 
2232         // switch order if needed, to match the mirrored part of the matrix
2233         if (!isOptypeAAlphaNumericSmaller) {
2234             helper = opSpecA;
2235             opSpecA = opSpecB;
2236             opSpecB = helper;
2237         }
2238         // look up transformation method
2239         transformationFunctionMap = transformations[opSpecA.optype];
2240         transformationFunction = transformationFunctionMap && transformationFunctionMap[opSpecB.optype];
2241 
2242         // transform
2243         if (transformationFunction) {
2244             result = transformationFunction(opSpecA, opSpecB, !isOptypeAAlphaNumericSmaller);
2245             if (!isOptypeAAlphaNumericSmaller && result !== null) {
2246                 // switch result back
2247                 result = {
2248                     opSpecsA:  result.opSpecsB,
2249                     opSpecsB:  result.opSpecsA
2250                 };
2251             }
2252         } else {
2253             result = null;
2254         }
2255 runtime.log("result:");
2256 if (result) {
2257 runtime.log(runtime.toJson(result.opSpecsA));
2258 runtime.log(runtime.toJson(result.opSpecsB));
2259 } else {
2260 runtime.log("null");
2261 }
2262         return result;
2263     };
2264 };
2265