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