UNPKG

node-webodf

Version:

WebODF - JavaScript Document Engine http://webodf.org/

1,136 lines (1,022 loc) 104 kB
/** * Copyright (C) 2013 KO GmbH <copyright@kogmbh.com> * * @licstart * This file is part of WebODF. * * WebODF is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License (GNU AGPL) * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. * * WebODF is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with WebODF. If not, see <http://www.gnu.org/licenses/>. * @licend * * @source: http://www.webodf.org/ * @source: https://github.com/kogmbh/WebODF/ */ /*global runtime, ops */ /** * @constructor */ ops.OperationTransformMatrix = function OperationTransformMatrix() { "use strict"; /* Utility methods */ /** * Inverts the range spanned up by the spec's parameter position and length, * so that position is at the other end of the range and length relative to that. * @param {!ops.OpMoveCursor.Spec} moveCursorSpec * @return {undefined} */ function invertMoveCursorSpecRange(moveCursorSpec) { moveCursorSpec.position = moveCursorSpec.position + moveCursorSpec.length; moveCursorSpec.length *= -1; } /** * Inverts the range spanned up by position and length if the length is negative. * Returns true if an inversion was done, false otherwise. * @param {!ops.OpMoveCursor.Spec} moveCursorSpec * @return {!boolean} */ function invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec) { var isBackwards = (moveCursorSpec.length < 0); if (isBackwards) { invertMoveCursorSpecRange(moveCursorSpec); } return isBackwards; } /** * Returns a list with all attributes in setProperties that refer to styleName * @param {?odf.Formatting.StyleData} setProperties * @param {!string} styleName * @return {!Array.<!string>} */ function getStyleReferencingAttributes(setProperties, styleName) { var attributes = []; /** * @param {string} attributeName */ function check(attributeName) { if (setProperties[attributeName] === styleName) { attributes.push(attributeName); } } if (setProperties) { ['style:parent-style-name', 'style:next-style-name'].forEach(check); } return attributes; } /** * @param {?odf.Formatting.StyleData} setProperties * @param {!string} deletedStyleName * @return {undefined} */ function dropStyleReferencingAttributes(setProperties, deletedStyleName) { /** * @param {string} attributeName */ function del(attributeName) { if (setProperties[attributeName] === deletedStyleName) { delete setProperties[attributeName]; } } if (setProperties) { ['style:parent-style-name', 'style:next-style-name'].forEach(del); } } /** * Creates a deep copy of the opspec * @param {!Object} opspec * @return {!Object} */ function cloneOpspec(opspec) { var result = {}; Object.keys(opspec).forEach(function (key) { if (typeof opspec[key] === 'object') { result[key] = cloneOpspec(opspec[key]); } else { result[key] = opspec[key]; } }); return result; } /** * @param {?Object.<string,*>} minorSetProperties * @param {?{attributes:string}} minorRemovedProperties * @param {?Object.<string,*>} majorSetProperties * @param {?{attributes:string}} majorRemovedProperties * @return {!{majorChanged:boolean,minorChanged:boolean}} */ function dropOverruledAndUnneededAttributes(minorSetProperties, minorRemovedProperties, majorSetProperties, majorRemovedProperties) { var i, name, majorChanged = false, minorChanged = false, removedPropertyNames, /**@type{!Array.<string>}*/ majorRemovedPropertyNames = []; if (majorRemovedProperties && majorRemovedProperties.attributes) { majorRemovedPropertyNames = majorRemovedProperties.attributes.split(','); } // iterate over all properties and see which get overwritten or deleted // by the overruling, so they have to be dropped if (minorSetProperties && (majorSetProperties || majorRemovedPropertyNames.length > 0)) { Object.keys(minorSetProperties).forEach(function (key) { var value = minorSetProperties[key], overrulingPropertyValue; // TODO: support more than one level if (typeof value !== "object") { if (majorSetProperties) { overrulingPropertyValue = majorSetProperties[key]; } if (overrulingPropertyValue !== undefined) { // drop overruled delete minorSetProperties[key]; minorChanged = true; // major sets to same value? if (overrulingPropertyValue === value) { // drop major as well delete majorSetProperties[key]; majorChanged = true; } } else if (majorRemovedPropertyNames.indexOf(key) !== -1) { // drop overruled delete minorSetProperties[key]; minorChanged = true; } } }); } // iterate over all overruling removed properties and drop any duplicates from // the removed property names if (minorRemovedProperties && minorRemovedProperties.attributes && (majorSetProperties || majorRemovedPropertyNames.length > 0)) { removedPropertyNames = minorRemovedProperties.attributes.split(','); for (i = 0; i < removedPropertyNames.length; i += 1) { name = removedPropertyNames[i]; if ((majorSetProperties && majorSetProperties[name] !== undefined) || (majorRemovedPropertyNames && majorRemovedPropertyNames.indexOf(name) !== -1)) { // drop removedPropertyNames.splice(i, 1); i -= 1; minorChanged = true; } } // set back if (removedPropertyNames.length > 0) { minorRemovedProperties.attributes = removedPropertyNames.join(','); } else { delete minorRemovedProperties.attributes; } } return { majorChanged: majorChanged, minorChanged: minorChanged }; } /** * Estimates if there are any properties set in the given properties object. * @param {!odf.Formatting.StyleData} properties * @return {!boolean} */ function hasProperties(properties) { var /**@type{string}*/ key; for (key in properties) { if (properties.hasOwnProperty(key)) { return true; } } return false; } /** * Estimates if there are any properties set in the given properties object. * @param {!{attributes:string}} properties * @return {!boolean} */ function hasRemovedProperties(properties) { var /**@type{string}*/ key; for (key in properties) { if (properties.hasOwnProperty(key)) { // handle empty 'attribute' as not existing if (key !== 'attributes' || properties.attributes.length > 0) { return true; } } } return false; } /** * @param {?odf.Formatting.StyleData} minorSet * @param {?Object.<string,{attributes:string}>} minorRem * @param {?odf.Formatting.StyleData} majorSet * @param {?Object.<string,{attributes:string}>} majorRem * @param {!string} propertiesName * @return {?{majorChanged:boolean,minorChanged:boolean}} */ function dropOverruledAndUnneededProperties(minorSet, minorRem, majorSet, majorRem, propertiesName) { var minorSP = /**@type{?odf.Formatting.StyleData}*/(minorSet ? minorSet[propertiesName] : null), minorRP = minorRem ? minorRem[propertiesName] : null, majorSP = /**@type{?odf.Formatting.StyleData}*/(majorSet ? majorSet[propertiesName] : null), majorRP = majorRem ? majorRem[propertiesName] : null, result; // TODO: also care for nested properties, like there can be e.g. with text:paragraph-properties result = dropOverruledAndUnneededAttributes(minorSP, minorRP, majorSP, majorRP); // remove empty setProperties if (minorSP && !hasProperties(minorSP)) { delete minorSet[propertiesName]; } // remove empty removedProperties if (minorRP && !hasRemovedProperties(minorRP)) { delete minorRem[propertiesName]; } // remove empty setProperties if (majorSP && !hasProperties(majorSP)) { delete majorSet[propertiesName]; } // remove empty removedProperties if (majorRP && !hasRemovedProperties(majorRP)) { delete majorRem[propertiesName]; } return result; } /* Transformation methods */ /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpecA * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpecB * @param {!boolean} hasAPriority * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationAddAnnotation(addAnnotationSpecA, addAnnotationSpecB, hasAPriority) { var firstAnnotationSpec, secondAnnotationSpec; if (addAnnotationSpecA.position < addAnnotationSpecB.position) { firstAnnotationSpec = addAnnotationSpecA; secondAnnotationSpec = addAnnotationSpecB; } else if (addAnnotationSpecB.position < addAnnotationSpecA.position) { firstAnnotationSpec = addAnnotationSpecB; secondAnnotationSpec = addAnnotationSpecA; } else { firstAnnotationSpec = hasAPriority ? addAnnotationSpecA : addAnnotationSpecB; secondAnnotationSpec = hasAPriority ? addAnnotationSpecB : addAnnotationSpecA; } if (secondAnnotationSpec.position < firstAnnotationSpec.position + firstAnnotationSpec.length) { firstAnnotationSpec.length += 2; } secondAnnotationSpec.position += 2; return { opSpecsA: [addAnnotationSpecA], opSpecsB: [addAnnotationSpecB] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationApplyDirectStyling(addAnnotationSpec, applyDirectStylingSpec) { if (addAnnotationSpec.position <= applyDirectStylingSpec.position) { applyDirectStylingSpec.position += 2; } else if (addAnnotationSpec.position <= applyDirectStylingSpec.position + applyDirectStylingSpec.length) { applyDirectStylingSpec.length += 2; } return { opSpecsA: [addAnnotationSpec], opSpecsB: [applyDirectStylingSpec] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpInsertText.Spec} insertTextSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationInsertText(addAnnotationSpec, insertTextSpec) { if (insertTextSpec.position <= addAnnotationSpec.position) { addAnnotationSpec.position += insertTextSpec.text.length; } else { if (addAnnotationSpec.length !== undefined) { if (insertTextSpec.position <= addAnnotationSpec.position + addAnnotationSpec.length) { addAnnotationSpec.length += insertTextSpec.text.length; } } // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range insertTextSpec.position += 2; } return { opSpecsA: [addAnnotationSpec], opSpecsB: [insertTextSpec] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationMergeParagraph(addAnnotationSpec, mergeParagraphSpec) { if (mergeParagraphSpec.sourceStartPosition <= addAnnotationSpec.position) { addAnnotationSpec.position -= 1; } else { if (addAnnotationSpec.length !== undefined) { if (mergeParagraphSpec.sourceStartPosition <= addAnnotationSpec.position + addAnnotationSpec.length) { addAnnotationSpec.length -= 1; } } // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range mergeParagraphSpec.sourceStartPosition += 2; if (addAnnotationSpec.position < mergeParagraphSpec.destinationStartPosition) { mergeParagraphSpec.destinationStartPosition += 2; } } return { opSpecsA: [addAnnotationSpec], opSpecsB: [mergeParagraphSpec] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpMoveCursor.Spec} moveCursorSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationMoveCursor(addAnnotationSpec, moveCursorSpec) { var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec); // adapt movecursor spec to inserted positions if (addAnnotationSpec.position < moveCursorSpec.position) { // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range moveCursorSpec.position += 2; } else if (addAnnotationSpec.position < moveCursorSpec.position + moveCursorSpec.length) { // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range moveCursorSpec.length += 2; } if (isMoveCursorSpecRangeInverted) { invertMoveCursorSpecRange(moveCursorSpec); } return { opSpecsA: [addAnnotationSpec], opSpecsB: [moveCursorSpec] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationRemoveAnnotation(addAnnotationSpec, removeAnnotationSpec) { // adapt movecursor spec to inserted positions if (addAnnotationSpec.position < removeAnnotationSpec.position) { if (removeAnnotationSpec.position < addAnnotationSpec.position + addAnnotationSpec.length) { addAnnotationSpec.length -= removeAnnotationSpec.length + 2; } // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range removeAnnotationSpec.position += 2; } else { // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range addAnnotationSpec.position -= removeAnnotationSpec.length + 2; } return { opSpecsA: [addAnnotationSpec], opSpecsB: [removeAnnotationSpec] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpRemoveText.Spec} removeTextSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationRemoveText(addAnnotationSpec, removeTextSpec) { var removeTextSpecPosition = removeTextSpec.position, removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length, annotationSpecEnd, helperOpspec, addAnnotationSpecResult = [addAnnotationSpec], removeTextSpecResult = [removeTextSpec]; // adapt removeTextSpec if (addAnnotationSpec.position <= removeTextSpec.position) { // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range removeTextSpec.position += 2; } else if (addAnnotationSpec.position < removeTextSpecEnd) { // we have to split the removal into two ops, before and after the annotation start removeTextSpec.length = addAnnotationSpec.position - removeTextSpec.position; helperOpspec = { optype: "RemoveText", memberid: removeTextSpec.memberid, timestamp: removeTextSpec.timestamp, position: addAnnotationSpec.position + 2, length: removeTextSpecEnd - addAnnotationSpec.position }; removeTextSpecResult.unshift(helperOpspec); // helperOp first, so its position is not affected by the real op } // adapt addAnnotationSpec (using already changed removeTextSpec and new helperOpspec, be aware) if (removeTextSpec.position + removeTextSpec.length <= addAnnotationSpec.position) { addAnnotationSpec.position -= removeTextSpec.length; if ((addAnnotationSpec.length !== undefined) && helperOpspec) { if (helperOpspec.length >= addAnnotationSpec.length) { addAnnotationSpec.length = 0; } else { addAnnotationSpec.length -= helperOpspec.length; } } } else if (addAnnotationSpec.length !== undefined) { annotationSpecEnd = addAnnotationSpec.position + addAnnotationSpec.length; if (removeTextSpecEnd <= annotationSpecEnd) { addAnnotationSpec.length -= removeTextSpec.length; } else if (removeTextSpecPosition < annotationSpecEnd) { addAnnotationSpec.length = removeTextSpecPosition - addAnnotationSpec.position; } } return { opSpecsA: addAnnotationSpecResult, opSpecsB: removeTextSpecResult }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationSetParagraphStyle(addAnnotationSpec, setParagraphStyleSpec) { if (addAnnotationSpec.position < setParagraphStyleSpec.position) { setParagraphStyleSpec.position += 2; } return { opSpecsA: [addAnnotationSpec], opSpecsB: [setParagraphStyleSpec] }; } /** * @param {!ops.OpAddAnnotation.Spec} addAnnotationSpec * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddAnnotationSplitParagraph(addAnnotationSpec, splitParagraphSpec) { if (addAnnotationSpec.position < splitParagraphSpec.sourceParagraphPosition) { splitParagraphSpec.sourceParagraphPosition += 2; } if (splitParagraphSpec.position <= addAnnotationSpec.position) { addAnnotationSpec.position += 1; } else { if (addAnnotationSpec.length !== undefined) { if (splitParagraphSpec.position <= addAnnotationSpec.position + addAnnotationSpec.length) { addAnnotationSpec.length += 1; } } // 2, because 1 for pos inside annotation comment, 1 for new pos before annotated range splitParagraphSpec.position += 2; } return { opSpecsA: [addAnnotationSpec], opSpecsB: [splitParagraphSpec] }; } /** * @param {!ops.OpAddStyle.Spec} addStyleSpec * @param {!ops.OpRemoveStyle.Spec} removeStyleSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformAddStyleRemoveStyle(addStyleSpec, removeStyleSpec) { var setAttributes, helperOpspec, addStyleSpecResult = [addStyleSpec], removeStyleSpecResult = [removeStyleSpec]; if (addStyleSpec.styleFamily === removeStyleSpec.styleFamily) { // deleted style brought into use by addstyle op? setAttributes = getStyleReferencingAttributes(addStyleSpec.setProperties, removeStyleSpec.styleName); if (setAttributes.length > 0) { // just create a updateparagraph style op preceding to us which removes any set style from the paragraph helperOpspec = { optype: "UpdateParagraphStyle", memberid: removeStyleSpec.memberid, timestamp: removeStyleSpec.timestamp, styleName: addStyleSpec.styleName, removedProperties: { attributes: setAttributes.join(',') } }; removeStyleSpecResult.unshift(helperOpspec); } // in the addstyle op drop any attributes referencing the style deleted dropStyleReferencingAttributes(addStyleSpec.setProperties, removeStyleSpec.styleName); } return { opSpecsA: addStyleSpecResult, opSpecsB: removeStyleSpecResult }; } /** * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpecA * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpecB * @param {!boolean} hasAPriority * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformApplyDirectStylingApplyDirectStyling(applyDirectStylingSpecA, applyDirectStylingSpecB, hasAPriority) { var majorSpec, minorSpec, majorSpecResult, minorSpecResult, majorSpecEnd, minorSpecEnd, dropResult, originalMajorSpec, originalMinorSpec, helperOpspecBefore, helperOpspecAfter, applyDirectStylingSpecAResult = [applyDirectStylingSpecA], applyDirectStylingSpecBResult = [applyDirectStylingSpecB]; // overlapping and any conflicting attributes? if (!(applyDirectStylingSpecA.position + applyDirectStylingSpecA.length <= applyDirectStylingSpecB.position || applyDirectStylingSpecA.position >= applyDirectStylingSpecB.position + applyDirectStylingSpecB.length)) { // adapt to priority majorSpec = hasAPriority ? applyDirectStylingSpecA : applyDirectStylingSpecB; minorSpec = hasAPriority ? applyDirectStylingSpecB : applyDirectStylingSpecA; // might need original opspecs? if (applyDirectStylingSpecA.position !== applyDirectStylingSpecB.position || applyDirectStylingSpecA.length !== applyDirectStylingSpecB.length) { originalMajorSpec = cloneOpspec(majorSpec); originalMinorSpec = cloneOpspec(minorSpec); } // for the part that is overlapping reduce setProperties by the overruled properties dropResult = dropOverruledAndUnneededProperties( minorSpec.setProperties, null, majorSpec.setProperties, null, 'style:text-properties' ); if (dropResult.majorChanged || dropResult.minorChanged) { // split the less-priority op into several ops for the overlapping and non-overlapping ranges majorSpecResult = []; minorSpecResult = []; majorSpecEnd = majorSpec.position + majorSpec.length; minorSpecEnd = minorSpec.position + minorSpec.length; // find if there is a part before and if there is a part behind, // create range-adapted copies of the original opspec, if the spec has changed if (minorSpec.position < majorSpec.position) { if (dropResult.minorChanged) { helperOpspecBefore = cloneOpspec(/**@type{!Object}*/(originalMinorSpec)); helperOpspecBefore.length = majorSpec.position - minorSpec.position; minorSpecResult.push(helperOpspecBefore); minorSpec.position = majorSpec.position; minorSpec.length = minorSpecEnd - minorSpec.position; } } else if (majorSpec.position < minorSpec.position) { if (dropResult.majorChanged) { helperOpspecBefore = cloneOpspec(/**@type{!Object}*/(originalMajorSpec)); helperOpspecBefore.length = minorSpec.position - majorSpec.position; majorSpecResult.push(helperOpspecBefore); majorSpec.position = minorSpec.position; majorSpec.length = majorSpecEnd - majorSpec.position; } } if (minorSpecEnd > majorSpecEnd) { if (dropResult.minorChanged) { helperOpspecAfter = originalMinorSpec; helperOpspecAfter.position = majorSpecEnd; helperOpspecAfter.length = minorSpecEnd - majorSpecEnd; minorSpecResult.push(helperOpspecAfter); minorSpec.length = majorSpecEnd - minorSpec.position; } } else if (majorSpecEnd > minorSpecEnd) { if (dropResult.majorChanged) { helperOpspecAfter = originalMajorSpec; helperOpspecAfter.position = minorSpecEnd; helperOpspecAfter.length = majorSpecEnd - minorSpecEnd; majorSpecResult.push(helperOpspecAfter); majorSpec.length = minorSpecEnd - majorSpec.position; } } // check if there are any changes left and this op has not become a noop if (majorSpec.setProperties && hasProperties(majorSpec.setProperties)) { majorSpecResult.push(majorSpec); } // check if there are any changes left and this op has not become a noop if (minorSpec.setProperties && hasProperties(minorSpec.setProperties)) { minorSpecResult.push(minorSpec); } if (hasAPriority) { applyDirectStylingSpecAResult = majorSpecResult; applyDirectStylingSpecBResult = minorSpecResult; } else { applyDirectStylingSpecAResult = minorSpecResult; applyDirectStylingSpecBResult = majorSpecResult; } } } return { opSpecsA: applyDirectStylingSpecAResult, opSpecsB: applyDirectStylingSpecBResult }; } /** * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec * @param {!ops.OpInsertText.Spec} insertTextSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformApplyDirectStylingInsertText(applyDirectStylingSpec, insertTextSpec) { // adapt applyDirectStyling spec to inserted positions if (insertTextSpec.position <= applyDirectStylingSpec.position) { applyDirectStylingSpec.position += insertTextSpec.text.length; } else if (insertTextSpec.position <= applyDirectStylingSpec.position + applyDirectStylingSpec.length) { applyDirectStylingSpec.length += insertTextSpec.text.length; } return { opSpecsA: [applyDirectStylingSpec], opSpecsB: [insertTextSpec] }; } /** * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformApplyDirectStylingMergeParagraph(applyDirectStylingSpec, mergeParagraphSpec) { var pointA = applyDirectStylingSpec.position, pointB = applyDirectStylingSpec.position + applyDirectStylingSpec.length; // adapt applyDirectStyling spec to merged paragraph if (pointA >= mergeParagraphSpec.sourceStartPosition) { pointA -= 1; } if (pointB >= mergeParagraphSpec.sourceStartPosition) { pointB -= 1; } applyDirectStylingSpec.position = pointA; applyDirectStylingSpec.length = pointB - pointA; return { opSpecsA: [applyDirectStylingSpec], opSpecsB: [mergeParagraphSpec] }; } /** * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformApplyDirectStylingRemoveAnnotation(applyDirectStylingSpec, removeAnnotationSpec) { var pointA = applyDirectStylingSpec.position, pointB = applyDirectStylingSpec.position + applyDirectStylingSpec.length, removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length, applyDirectStylingSpecResult = [applyDirectStylingSpec], removeAnnotationSpecResult = [removeAnnotationSpec]; // check if inside removed annotation if (removeAnnotationSpec.position <= pointA && pointB <= removeAnnotationEnd) { applyDirectStylingSpecResult = []; } else { // adapt applyDirectStyling spec to removed annotation content if (removeAnnotationEnd < pointA) { pointA -= removeAnnotationSpec.length + 2; } if (removeAnnotationEnd < pointB) { pointB -= removeAnnotationSpec.length + 2; } applyDirectStylingSpec.position = pointA; applyDirectStylingSpec.length = pointB - pointA; } return { opSpecsA: applyDirectStylingSpecResult, opSpecsB: removeAnnotationSpecResult }; } /** * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec * @param {!ops.OpRemoveText.Spec} removeTextSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformApplyDirectStylingRemoveText(applyDirectStylingSpec, removeTextSpec) { var applyDirectStylingSpecEnd = applyDirectStylingSpec.position + applyDirectStylingSpec.length, removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length, applyDirectStylingSpecResult = [applyDirectStylingSpec], removeTextSpecResult = [removeTextSpec]; // transform applyDirectStylingSpec // removed positions by object up to move cursor position? if (removeTextSpecEnd <= applyDirectStylingSpec.position) { // adapt by removed position applyDirectStylingSpec.position -= removeTextSpec.length; // overlapping? } else if (removeTextSpec.position < applyDirectStylingSpecEnd) { // still to select range starting at cursor position? if (applyDirectStylingSpec.position < removeTextSpec.position) { // still to select range ending at selection? if (removeTextSpecEnd < applyDirectStylingSpecEnd) { applyDirectStylingSpec.length -= removeTextSpec.length; } else { applyDirectStylingSpec.length = removeTextSpec.position - applyDirectStylingSpec.position; } // remove overlapping section } else { // fall at start of removed section applyDirectStylingSpec.position = removeTextSpec.position; // still to select range at selection end? if (removeTextSpecEnd < applyDirectStylingSpecEnd) { applyDirectStylingSpec.length = applyDirectStylingSpecEnd - removeTextSpecEnd; } else { // completely overlapped by other, so becomes no-op // TODO: once we can address spans, removeTextSpec would need to get a helper op // to remove the empty span left over applyDirectStylingSpecResult = []; } } } return { opSpecsA: applyDirectStylingSpecResult, opSpecsB: removeTextSpecResult }; } /** * @param {!ops.OpApplyDirectStyling.Spec} applyDirectStylingSpec * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformApplyDirectStylingSplitParagraph(applyDirectStylingSpec, splitParagraphSpec) { // transform applyDirectStylingSpec if (splitParagraphSpec.position < applyDirectStylingSpec.position) { applyDirectStylingSpec.position += 1; } else if (splitParagraphSpec.position < applyDirectStylingSpec.position + applyDirectStylingSpec.length) { applyDirectStylingSpec.length += 1; } return { opSpecsA: [applyDirectStylingSpec], opSpecsB: [splitParagraphSpec] }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpecA * @param {!ops.OpInsertText.Spec} insertTextSpecB * @param {!boolean} hasAPriority * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextInsertText(insertTextSpecA, insertTextSpecB, hasAPriority) { if (insertTextSpecA.position < insertTextSpecB.position) { insertTextSpecB.position += insertTextSpecA.text.length; } else if (insertTextSpecA.position > insertTextSpecB.position) { insertTextSpecA.position += insertTextSpecB.text.length; } else { if (hasAPriority) { insertTextSpecB.position += insertTextSpecA.text.length; } else { insertTextSpecA.position += insertTextSpecB.text.length; } } return { opSpecsA: [insertTextSpecA], opSpecsB: [insertTextSpecB] }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextMergeParagraph(insertTextSpec, mergeParagraphSpec) { if (insertTextSpec.position >= mergeParagraphSpec.sourceStartPosition) { insertTextSpec.position -= 1; } else { if (insertTextSpec.position < mergeParagraphSpec.sourceStartPosition) { mergeParagraphSpec.sourceStartPosition += insertTextSpec.text.length; } if (insertTextSpec.position < mergeParagraphSpec.destinationStartPosition) { mergeParagraphSpec.destinationStartPosition += insertTextSpec.text.length; } } return { opSpecsA: [insertTextSpec], opSpecsB: [mergeParagraphSpec] }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpMoveCursor.Spec} moveCursorSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextMoveCursor(insertTextSpec, moveCursorSpec) { var isMoveCursorSpecRangeInverted = invertMoveCursorSpecRangeOnNegativeLength(moveCursorSpec); // adapt movecursor spec to inserted positions if (insertTextSpec.position < moveCursorSpec.position) { moveCursorSpec.position += insertTextSpec.text.length; } else if (insertTextSpec.position < moveCursorSpec.position + moveCursorSpec.length) { moveCursorSpec.length += insertTextSpec.text.length; } if (isMoveCursorSpecRangeInverted) { invertMoveCursorSpecRange(moveCursorSpec); } return { opSpecsA: [insertTextSpec], opSpecsB: [moveCursorSpec] }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextRemoveAnnotation(insertTextSpec, removeAnnotationSpec) { var insertTextSpecPosition = insertTextSpec.position, removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length, insertTextSpecResult = [insertTextSpec], removeAnnotationSpecResult = [removeAnnotationSpec]; // check if inside removed annotation if (removeAnnotationSpec.position <= insertTextSpecPosition && insertTextSpecPosition <= removeAnnotationEnd) { insertTextSpecResult = []; removeAnnotationSpec.length += insertTextSpec.text.length; } else { // adapt insertText spec to removed annotation content if (removeAnnotationEnd < insertTextSpec.position) { insertTextSpec.position -= removeAnnotationSpec.length + 2; } else { removeAnnotationSpec.position += insertTextSpec.text.length; } } return { opSpecsA: insertTextSpecResult, opSpecsB: removeAnnotationSpecResult }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpRemoveText.Spec} removeTextSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextRemoveText(insertTextSpec, removeTextSpec) { var helperOpspec, removeTextSpecEnd = removeTextSpec.position + removeTextSpec.length, insertTextSpecResult = [insertTextSpec], removeTextSpecResult = [removeTextSpec]; // update insertTextSpec // removed before/up to insertion point? if (removeTextSpecEnd <= insertTextSpec.position) { insertTextSpec.position -= removeTextSpec.length; // removed at/behind insertion point } else if (insertTextSpec.position <= removeTextSpec.position) { removeTextSpec.position += insertTextSpec.text.length; // insertion in middle of removed range } else { // we have to split the removal into two ops, before and after the insertion point removeTextSpec.length = insertTextSpec.position - removeTextSpec.position; helperOpspec = { optype: "RemoveText", memberid: removeTextSpec.memberid, timestamp: removeTextSpec.timestamp, position: insertTextSpec.position + insertTextSpec.text.length, length: removeTextSpecEnd - insertTextSpec.position }; removeTextSpecResult.unshift(helperOpspec); // helperOp first, so its position is not affected by the real op // drop insertion point to begin of removed range // original insertTextSpec.position is used for removeTextSpec changes, so only change now insertTextSpec.position = removeTextSpec.position; } return { opSpecsA: insertTextSpecResult, opSpecsB: removeTextSpecResult }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpSetParagraphStyle.Spec} setParagraphStyleSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextSetParagraphStyle(insertTextSpec, setParagraphStyleSpec) { if (setParagraphStyleSpec.position > insertTextSpec.position) { setParagraphStyleSpec.position += insertTextSpec.text.length; } return { opSpecsA: [insertTextSpec], opSpecsB: [setParagraphStyleSpec] }; } /** * @param {!ops.OpInsertText.Spec} insertTextSpec * @param {!ops.OpSplitParagraph.Spec} splitParagraphSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformInsertTextSplitParagraph(insertTextSpec, splitParagraphSpec) { if (insertTextSpec.position < splitParagraphSpec.sourceParagraphPosition) { splitParagraphSpec.sourceParagraphPosition += insertTextSpec.text.length; } if (insertTextSpec.position <= splitParagraphSpec.position) { splitParagraphSpec.position += insertTextSpec.text.length; } else { insertTextSpec.position += 1; } return { opSpecsA: [insertTextSpec], opSpecsB: [splitParagraphSpec] }; } /** * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpecA * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpecB * @param {!boolean} hasAPriority * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformMergeParagraphMergeParagraph(mergeParagraphSpecA, mergeParagraphSpecB, hasAPriority) { var specsForB = [mergeParagraphSpecA], specsForA = [mergeParagraphSpecB], priorityOp, styleParagraphFixup, moveCursorA, moveCursorB; if (mergeParagraphSpecA.destinationStartPosition === mergeParagraphSpecB.destinationStartPosition) { // Two merge commands for the same paragraph result in a noop to both sides, as the same // paragraph can only be merged once. specsForB = []; specsForA = []; // If the moveCursor flag is set, the cursor will still need to be adjusted to the right location if (mergeParagraphSpecA.moveCursor) { moveCursorA = /**@type{!ops.OpMoveCursor.Spec}*/({ optype: "MoveCursor", memberid: mergeParagraphSpecA.memberid, timestamp: mergeParagraphSpecA.timestamp, position: mergeParagraphSpecA.sourceStartPosition - 1 }); specsForB.push(moveCursorA); } if (mergeParagraphSpecB.moveCursor) { moveCursorB = /**@type{!ops.OpMoveCursor.Spec}*/({ optype: "MoveCursor", memberid: mergeParagraphSpecB.memberid, timestamp: mergeParagraphSpecB.timestamp, position: mergeParagraphSpecB.sourceStartPosition - 1 }); specsForA.push(moveCursorB); } // Determine which merge style wins priorityOp = hasAPriority ? mergeParagraphSpecA : mergeParagraphSpecB; styleParagraphFixup = /**@type{!ops.OpSetParagraphStyle.Spec}*/({ optype: "SetParagraphStyle", memberid: priorityOp.memberid, timestamp: priorityOp.timestamp, position: priorityOp.destinationStartPosition, styleName: priorityOp.paragraphStyleName }); if (hasAPriority) { specsForB.push(styleParagraphFixup); } else { specsForA.push(styleParagraphFixup); } } else if (mergeParagraphSpecB.sourceStartPosition === mergeParagraphSpecA.destinationStartPosition) { // Two consecutive paragraphs are being merged. E.g., A <- B <- C. // Use the styleName of the lowest destination paragraph to set the paragraph style (A <- B) mergeParagraphSpecA.destinationStartPosition = mergeParagraphSpecB.destinationStartPosition; mergeParagraphSpecA.sourceStartPosition -= 1; mergeParagraphSpecA.paragraphStyleName = mergeParagraphSpecB.paragraphStyleName; } else if (mergeParagraphSpecA.sourceStartPosition === mergeParagraphSpecB.destinationStartPosition) { // Two consecutive paragraphs are being merged. E.g., A <- B <- C. // Use the styleName of the lowest destination paragraph to set the paragraph style (A <- B) mergeParagraphSpecB.destinationStartPosition = mergeParagraphSpecA.destinationStartPosition; mergeParagraphSpecB.sourceStartPosition -= 1; mergeParagraphSpecB.paragraphStyleName = mergeParagraphSpecA.paragraphStyleName; } else if (mergeParagraphSpecA.destinationStartPosition < mergeParagraphSpecB.destinationStartPosition) { mergeParagraphSpecB.destinationStartPosition -= 1; mergeParagraphSpecB.sourceStartPosition -= 1; } else { // mergeParagraphSpecB.destinationStartPosition < mergeParagraphSpecA.destinationStartPosition mergeParagraphSpecA.destinationStartPosition -= 1; mergeParagraphSpecA.sourceStartPosition -= 1; } return { opSpecsA: specsForB, opSpecsB: specsForA }; } /** * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec * @param {!ops.OpMoveCursor.Spec} moveCursorSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformMergeParagraphMoveCursor(mergeParagraphSpec, moveCursorSpec) { var pointA = moveCursorSpec.position, pointB = moveCursorSpec.position + moveCursorSpec.length, start = Math.min(pointA, pointB), end = Math.max(pointA, pointB); if (start >= mergeParagraphSpec.sourceStartPosition) { start -= 1; } if (end >= mergeParagraphSpec.sourceStartPosition) { end -= 1; } // When updating the cursor spec, ensure the selection direction is preserved. // If the length was previously positive, it should remain positive. if (moveCursorSpec.length >= 0) { moveCursorSpec.position = start; moveCursorSpec.length = end - start; } else { moveCursorSpec.position = end; moveCursorSpec.length = start - end; } return { opSpecsA: [mergeParagraphSpec], opSpecsB: [moveCursorSpec] }; } /** * @param {!ops.OpMergeParagraph.Spec} mergeParagraphSpec * @param {!ops.OpRemoveAnnotation.Spec} removeAnnotationSpec * @return {?{opSpecsA:!Array.<!Object>, opSpecsB:!Array.<!Object>}} */ function transformMergeParagraphRemoveAnnotation(mergeParagraphSpec, removeAnnotationSpec) { var removeAnnotationEnd = removeAnnotationSpec.position + removeAnnotationSpec.length, mergeParagraphSpecResult = [mergeParagraphSpec], removeAnnotationSpecResult = [removeAnnotationSpec]; // check if inside removed annotation if (removeAnnotationSpec.position <= mergeParagraphSpec.destinationStartPosition && mergeParagraphSpec.sourceStartPosition <= removeAnnotationEnd) { mergeParagraphSpecResult = []; removeAnnotationSpec.length -= 1; } else { if (mergeParagraphSpec.sourceStartPosition < removeAnnotationSpec.position) { removeAnnotationSpec.position -= 1; } else { if (removeAnnotationEnd < mergeParagraphSpec.destinationStartPosition) { mergeParagraphSpec.destinationStartPosition -= removeAnnotationSpec.length + 2; } if (removeAnnotationEnd < mergeParagraphSpec.sourceStartPosition) { mergeParagraphSpec.sourceStartPosition -= removeAnnotationSpec.length + 2;