node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
1,136 lines (1,022 loc) • 104 kB
JavaScript
/**
* 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;