diff --git a/CHANGELOG.MD b/CHANGELOG.MD new file mode 100644 index 0000000..6e93522 --- /dev/null +++ b/CHANGELOG.MD @@ -0,0 +1 @@ +21/4/2020 - changing around line 146 in text0 to also transform position when ownOp and position = op to address bugs with code cursors diff --git a/lib/json0.js b/lib/json0.js index dc3a405..c7799bb 100644 --- a/lib/json0.js +++ b/lib/json0.js @@ -651,6 +651,138 @@ json.transformComponent = function(dest, c, otherC, type) { return dest; }; +json.createPresence = function(presenceData) { + return presenceData; +}; + +json.comparePresence = function(pres1, pres2) { + return JSON.stringify(pres1) === JSON.stringify(pres2); +}; + +// var transformPosition = function(cursor, op, isOwnOp) { +// var cursor = clone(cursor); +// +// var opIsAncestor = cursor.length >= op.p.length; // true also if op is self +// var opIsSibling = cursor.length === op.p.length; // true also if op is self +// var opIsAncestorSibling = cursor.length >= op.p.length; // true also if op is self or sibling of self +// var equalUpTo = -1; +// for (var i = 0; i < op.p.length; i++) { +// if (op.p[i] !== cursor[i]) { +// opIsAncestor = false; +// if (i < op.p.length - 1) { +// opIsSibling = false; +// opIsAncestorSibling = false; +// } +// } +// if (equalUpTo === i - 1 && op.p[i] === cursor[i]) { +// equalUpTo += 1; +// } +// } +// +// if (opIsSibling) { +// if (op.sd) { +// cursor[cursor.length - 1] = text.transformCursor( +// cursor[cursor.length - 1], +// [{ p: op.p[op.p.length - 1], d: op.sd }], +// isOwnOp ? 'right' : 'left' +// ); +// } +// if (op.si) { +// cursor[cursor.length - 1] = text.transformCursor( +// cursor[cursor.length - 1], +// [{ p: op.p[op.p.length - 1], i: op.si }], +// isOwnOp ? 'right' : 'left' +// ); +// } +// } +// +// if (opIsAncestor) { +// if (op.lm !== undefined) { +// cursor[equalUpTo] = op.lm; +// } +// if (op.od && op.oi) { +// cursor = op.p.slice(0, op.p.length); +// } else if (op.od) { +// cursor = op.p.slice(0, op.p.length - 1); +// } else if (op.ld && op.li) { +// cursor = op.p.slice(0, op.p.length); +// } else if (op.ld) { +// cursor = op.p.slice(0, op.p.length - 1); +// } +// } +// +// if (opIsAncestorSibling) { +// var lastPathIdx = op.p.length - 1; +// if ( +// !opIsAncestor && +// op.ld && +// !op.li && +// op.p[lastPathIdx] < cursor[lastPathIdx] +// ) { +// cursor[lastPathIdx] -= 1; +// } else if (!op.ld && op.li && op.p[lastPathIdx] <= cursor[lastPathIdx]) { +// cursor[lastPathIdx] += 1; +// } +// +// // if move item in list from after to before +// if ( +// !opIsAncestor && +// op.lm !== undefined && +// op.p[lastPathIdx] > cursor[lastPathIdx] && +// op.lm <= cursor[lastPathIdx] +// ) { +// cursor[lastPathIdx] += 1; +// // if move item in list from before to after +// } else if ( +// !opIsAncestor && +// op.lm !== undefined && +// op.p[lastPathIdx] < cursor[lastPathIdx] && +// op.lm >= cursor[lastPathIdx] +// ) { +// cursor[lastPathIdx] -= 1; +// } +// } +// +// return cursor; +// }; +// +// json.transformCursor = function(cursor, op, isOwnOp) { +// for (var i = 0; i < op.length; i++) { +// cursor = transformPosition(cursor, op[i], isOwnOp); +// } +// return cursor; +// }; +// + +json.transformPresence = function(presence, op, isOwnOp) { + // Don't transform path-only presence objects. + if(!presence.t) return presence; + + for (var i = 0; i < op.length; i++) { + var c = op[i]; + + // convert old string ops to use subtype for backwards compatibility + if (c.si != null || c.sd != null) { + convertFromText(c); + } + + // Transform against subtype ops. + if (c.t && c.t === presence.t && json.pathMatches(c.p, presence.p)) { + presence = Object.assign({}, presence, { + s: subtypes[presence.t].transformPresence(presence.s, [c], isOwnOp) + }); + } + + // convert back to old string ops + if (c.t === 'text0') { + convertToText(c); + } + + // TODO transform against non-subtype ops. + }; + return presence; +}; + require('./bootstrapTransform')(json, json.transformComponent, json.checkValidOp, json.append); /** diff --git a/lib/text0.js b/lib/text0.js index e26c6a9..3407ebf 100644 --- a/lib/text0.js +++ b/lib/text0.js @@ -163,6 +163,43 @@ var transformPosition = function(pos, c, insertAfter) { } }; +// This helper method transforms a position by an op component. +// +// If c is an insert, insertAfter specifies whether the transform +// is pushed after the insert (true) or before it (false). +// +// insertAfter is optional for deletes. +// Stian 21/4/2020: Originally there was insertAfter, but this caused a +// bug where selecting text, then deleting it by typing a letter would get +// misinterpreted. Seems to me that we want to push the cursor even by +// normal typing. Perhaps this is because we don't send position updates +// when typing, while others do? Hopefully changing this doesn't introduce +// other bugs. We duplicated the transformPosition function to not introduce +// bugs into the core OT mechanism. + +var transformPositionPresence = function(pos, c, insertAfter) { + // This will get collapsed into a giant ternary by uglify. + if (c.i != null) { + if (c.p < pos || (c.p === pos) { + return pos + c.i.length; + } else { + return pos; + } + } else { + // I think this could also be written as: Math.min(c.p, Math.min(c.p - + // otherC.p, otherC.d.length)) but I think its harder to read that way, and + // it compiles using ternary operators anyway so its no slower written like + // this. + if (pos <= c.p) { + return pos; + } else if (pos <= c.p + c.d.length) { + return c.p; + } else { + return pos - c.d.length; + } + } +}; + // Helper method to transform a cursor position as a result of an op. // // Like transformPosition above, if c is an insert, insertAfter specifies @@ -171,7 +208,7 @@ var transformPosition = function(pos, c, insertAfter) { text.transformCursor = function(position, op, side) { var insertAfter = side === 'right'; for (var i = 0; i < op.length; i++) { - position = transformPosition(position, op[i], insertAfter); + position = transformPositionPresence(position, op[i], insertAfter); } return position; @@ -253,4 +290,34 @@ text.invert = function(op) { return op; }; +text.createPresence = function(presenceData) { + return presenceData; +}; + +// Draws from https://github.com/Teamwork/ot-rich-text/blob/master/src/Operation.js +text.transformPresence = function(presence, operation, isOwnOperation) { + var user = presence.u; + var change = presence.c; + var selections = presence.s; + var side = isOwnOperation ? 'right' : 'left'; + var newSelections = new Array(selections.length); + + for (var i = 0, l = selections.length; i < l; ++i) { + newSelections[i] = [ + text.transformCursor(selections[i][0], operation[0].o, side), + text.transformCursor(selections[i][1], operation[0].o, side) + ]; + } + + return { + u: user, + c: change, + s: newSelections + } +} + +text.comparePresence = function(pres1, pres2) { + return JSON.stringify(pres1) === JSON.stringify(pres2); +}; + require('./bootstrapTransform')(text, transformComponent, checkValidOp, append); diff --git a/package.json b/package.json index b6c9df6..18be7c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "ot-json0", - "version": "1.1.0", + "name": "@minervaproject/ot-json0", + "version": "1.4.2", "description": "JSON OT type", "main": "lib/index.js", "directories": {