superpowers-game-ftext-plugin
Version:
Generic text asset for the Superpowers Game system of Superpowers, the extensible HTML5 2D+3D game engine.
1,003 lines (943 loc) • 37.6 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function (process){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// resolves . and .. elements in a path array with directory names there
// must be no slashes, empty elements, or device names (c:\) in the array
// (so also no leading and trailing slashes - it does not distinguish
// relative and absolute paths)
function normalizeArray(parts, allowAboveRoot) {
// if the path tries to go above the root, `up` ends up > 0
var up = 0;
for (var i = parts.length - 1; i >= 0; i--) {
var last = parts[i];
if (last === '.') {
parts.splice(i, 1);
} else if (last === '..') {
parts.splice(i, 1);
up++;
} else if (up) {
parts.splice(i, 1);
up--;
}
}
// if the path is allowed to go above the root, restore leading ..s
if (allowAboveRoot) {
for (; up--; up) {
parts.unshift('..');
}
}
return parts;
}
// Split a filename into [root, dir, basename, ext], unix version
// 'root' is just a slash, or nothing.
var splitPathRe =
/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;
var splitPath = function(filename) {
return splitPathRe.exec(filename).slice(1);
};
// path.resolve([from ...], to)
// posix version
exports.resolve = function() {
var resolvedPath = '',
resolvedAbsolute = false;
for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
var path = (i >= 0) ? arguments[i] : process.cwd();
// Skip empty and invalid entries
if (typeof path !== 'string') {
throw new TypeError('Arguments to path.resolve must be strings');
} else if (!path) {
continue;
}
resolvedPath = path + '/' + resolvedPath;
resolvedAbsolute = path.charAt(0) === '/';
}
// At this point the path should be resolved to a full absolute path, but
// handle relative paths to be safe (might happen when process.cwd() fails)
// Normalize the path
resolvedPath = normalizeArray(filter(resolvedPath.split('/'), function(p) {
return !!p;
}), !resolvedAbsolute).join('/');
return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.';
};
// path.normalize(path)
// posix version
exports.normalize = function(path) {
var isAbsolute = exports.isAbsolute(path),
trailingSlash = substr(path, -1) === '/';
// Normalize the path
path = normalizeArray(filter(path.split('/'), function(p) {
return !!p;
}), !isAbsolute).join('/');
if (!path && !isAbsolute) {
path = '.';
}
if (path && trailingSlash) {
path += '/';
}
return (isAbsolute ? '/' : '') + path;
};
// posix version
exports.isAbsolute = function(path) {
return path.charAt(0) === '/';
};
// posix version
exports.join = function() {
var paths = Array.prototype.slice.call(arguments, 0);
return exports.normalize(filter(paths, function(p, index) {
if (typeof p !== 'string') {
throw new TypeError('Arguments to path.join must be strings');
}
return p;
}).join('/'));
};
// path.relative(from, to)
// posix version
exports.relative = function(from, to) {
from = exports.resolve(from).substr(1);
to = exports.resolve(to).substr(1);
function trim(arr) {
var start = 0;
for (; start < arr.length; start++) {
if (arr[start] !== '') break;
}
var end = arr.length - 1;
for (; end >= 0; end--) {
if (arr[end] !== '') break;
}
if (start > end) return [];
return arr.slice(start, end - start + 1);
}
var fromParts = trim(from.split('/'));
var toParts = trim(to.split('/'));
var length = Math.min(fromParts.length, toParts.length);
var samePartsLength = length;
for (var i = 0; i < length; i++) {
if (fromParts[i] !== toParts[i]) {
samePartsLength = i;
break;
}
}
var outputParts = [];
for (var i = samePartsLength; i < fromParts.length; i++) {
outputParts.push('..');
}
outputParts = outputParts.concat(toParts.slice(samePartsLength));
return outputParts.join('/');
};
exports.sep = '/';
exports.delimiter = ':';
exports.dirname = function(path) {
var result = splitPath(path),
root = result[0],
dir = result[1];
if (!root && !dir) {
// No dirname whatsoever
return '.';
}
if (dir) {
// It has a dirname, strip trailing slash
dir = dir.substr(0, dir.length - 1);
}
return root + dir;
};
exports.basename = function(path, ext) {
var f = splitPath(path)[2];
// TODO: make this comparison case-insensitive on windows?
if (ext && f.substr(-1 * ext.length) === ext) {
f = f.substr(0, f.length - ext.length);
}
return f;
};
exports.extname = function(path) {
return splitPath(path)[3];
};
function filter (xs, f) {
if (xs.filter) return xs.filter(f);
var res = [];
for (var i = 0; i < xs.length; i++) {
if (f(xs[i], i, xs)) res.push(xs[i]);
}
return res;
}
// String.prototype.substr - negative index don't work in IE8
var substr = 'ab'.substr(-1) === 'b'
? function (str, start, len) { return str.substr(start, len) }
: function (str, start, len) {
if (start < 0) start = str.length + start;
return str.substr(start, len);
}
;
}).call(this,require('_process'))
},{"_process":2}],2:[function(require,module,exports){
// shim for using process in browser
var process = module.exports = {};
var queue = [];
var draining = false;
var currentQueue;
var queueIndex = -1;
function cleanUpNextTick() {
draining = false;
if (currentQueue.length) {
queue = currentQueue.concat(queue);
} else {
queueIndex = -1;
}
if (queue.length) {
drainQueue();
}
}
function drainQueue() {
if (draining) {
return;
}
var timeout = setTimeout(cleanUpNextTick);
draining = true;
var len = queue.length;
while(len) {
currentQueue = queue;
queue = [];
while (++queueIndex < len) {
if (currentQueue) {
currentQueue[queueIndex].run();
}
}
queueIndex = -1;
len = queue.length;
}
currentQueue = null;
draining = false;
clearTimeout(timeout);
}
process.nextTick = function (fun) {
var args = new Array(arguments.length - 1);
if (arguments.length > 1) {
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
}
queue.push(new Item(fun, args));
if (queue.length === 1 && !draining) {
setTimeout(drainQueue, 0);
}
};
// v8 likes predictible objects
function Item(fun, array) {
this.fun = fun;
this.array = array;
}
Item.prototype.run = function () {
this.fun.apply(null, this.array);
};
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
process.version = ''; // empty string to avoid regexp issues
process.versions = {};
function noop() {}
process.on = noop;
process.addListener = noop;
process.once = noop;
process.off = noop;
process.removeListener = noop;
process.removeAllListeners = noop;
process.emit = noop;
process.binding = function (name) {
throw new Error('process.binding is not supported');
};
process.cwd = function () { return '/' };
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
process.umask = function() { return 0; };
},{}],3:[function(require,module,exports){
var fTextSettingsResource = require("./fTextSettingsResource");
var fTextAsset = require("./fTextAsset");
SupCore.system.data.registerResource("fTextSettings", fTextSettingsResource.default);
SupCore.system.data.registerAssetClass("fText", fTextAsset.default);
},{"./fTextAsset":4,"./fTextSettingsResource":5}],4:[function(require,module,exports){
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var OT = require("operational-transform");
var path = require("path");
var fTextAsset = (function (_super) {
__extends(fTextAsset, _super);
// called from the editor onAssetReceived() as well as on server startup
function fTextAsset(id, pub, serverData) {
_super.call(this, id, pub, fTextAsset.schema, serverData);
}
// called on asset creation
// options contain the asset's name
fTextAsset.prototype.init = function (options, callback) {
var defaultContent = "";
this.pub = {
text: defaultContent,
draft: defaultContent,
revisionId: 0,
};
_super.prototype.init.call(this, options, callback);
};
fTextAsset.prototype.setup = function () {
this.document = new OT.Document(this.pub.draft, this.pub.revisionId);
this.hasDraft = this.pub.text !== this.pub.draft;
};
fTextAsset.prototype.restore = function () {
if (this.hasDraft)
this.emit("setBadge", "draft", "info");
};
fTextAsset.prototype.destroy = function (callback) {
callback();
};
// called on server startup
fTextAsset.prototype.load = function (assetPath) {
// NOTE: We must not set this.pub with temporary values here, otherwise
// the asset will be considered loaded by Dictionary.acquire
// and the acquire callback will be called immediately
var _this = this;
fs.readFile(path.join(assetPath, "ftext.txt"), { encoding: "utf8" }, function (err, text) {
fs.readFile(path.join(assetPath, "draft.txt"), { encoding: "utf8" }, function (err, draft) {
_this.pub = { revisionId: 0, text: text, draft: (draft != null) ? draft : text };
_this.setup();
_this.emit("load");
});
});
};
// called when it is time to write the asset on disk, not when the user save the asset from the editor
fTextAsset.prototype.save = function (assetPath, callback) {
var _this = this;
fs.writeFile(path.join(assetPath, "ftext.txt"), this.pub.text, { encoding: "utf8" }, function (err) {
if (err != null) {
callback(err);
return;
}
if (_this.hasDraft) {
fs.writeFile(path.join(assetPath, "draft.txt"), _this.pub.draft, { encoding: "utf8" }, callback);
}
else {
// delete the draft.txt file if there is no draft to save and the file exists
fs.unlink(path.join(assetPath, "draft.txt"), function (err) {
if (err != null && err.code !== "ENOENT") {
callback(err);
return;
}
callback(null);
});
}
});
};
fTextAsset.prototype.server_editText = function (client, operationData, revisionIndex, callback) {
if (operationData.userId !== client.id) {
callback("Invalid client id");
return;
}
var operation = new OT.TextOperation();
if (!operation.deserialize(operationData)) {
callback("Invalid operation data");
return;
}
try {
operation = this.document.apply(operation, revisionIndex);
}
catch (err) {
callback("Operation can't be applied");
return;
}
this.pub.draft = this.document.text;
this.pub.revisionId++;
callback(null, operation.serialize(), this.document.getRevisionId() - 1);
if (!this.hasDraft) {
this.hasDraft = true;
this.emit("setBadge", "draft", "info");
}
this.emit("change");
};
fTextAsset.prototype.client_editText = function (operationData, revisionIndex) {
var operation = new OT.TextOperation();
operation.deserialize(operationData);
this.document.apply(operation, revisionIndex);
this.pub.draft = this.document.text;
this.pub.revisionId++;
};
fTextAsset.prototype.server_saveText = function (client, callback) {
this.pub.text = this.pub.draft;
callback(null);
if (this.hasDraft) {
this.hasDraft = false;
this.emit("clearBadge", "draft");
}
this.emit("change");
};
fTextAsset.prototype.client_saveText = function () { this.pub.text = this.pub.draft; };
fTextAsset.schema = {
text: { type: "string" },
draft: { type: "string" },
revisionId: { type: "integer" },
};
return fTextAsset;
})(SupCore.Data.Base.Asset);
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = fTextAsset;
},{"operational-transform":9,"path":1}],5:[function(require,module,exports){
var __extends = (this && this.__extends) || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var fTextSettingsResource = (function (_super) {
__extends(fTextSettingsResource, _super);
function fTextSettingsResource(id, pub, serverData) {
_super.call(this, id, pub, fTextSettingsResource.schema, serverData);
}
fTextSettingsResource.prototype.init = function (callback) {
var pub = {};
for (var name_1 in fTextSettingsResource.defaultValues) {
pub[name_1] = fTextSettingsResource.defaultValues[name_1];
}
this.pub = pub;
_super.prototype.init.call(this, callback);
};
fTextSettingsResource.schema = {
theme: { type: "string", mutable: true },
customTheme: { type: "string", mutable: true },
tabSize: { type: "number", min: 1, max: 8, mutable: true },
indentWithTabs: { type: "boolean", mutable: true },
keyMap: { type: "enum", items: ["sublime", "vim", "emacs"], mutable: true },
styleActiveLine: { type: "boolean", mutable: true },
showTrailingSpace: { type: "boolean", mutable: true },
autoCloseBrackets: { type: "boolean", mutable: true },
matchTags: { type: "boolean", mutable: true },
highlightSelectionMatches: { type: "boolean", mutable: true },
lint_json: { type: "boolean", mutable: true },
lint_cson: { type: "boolean", mutable: true },
lint_javascript: { type: "boolean", mutable: true },
lint_jade: { type: "boolean", mutable: true },
lint_stylus: { type: "boolean", mutable: true },
lint_css: { type: "boolean", mutable: true },
lint_yaml: { type: "boolean", mutable: true },
};
fTextSettingsResource.defaultValues = {
theme: "default",
customTheme: "",
tabSize: 2,
indentWithTabs: true,
keyMap: "sublime",
styleActiveLine: true,
autoCloseBrackets: true,
showTrailingSpace: true,
matchTags: true,
highlightSelectionMatches: true,
lint_json: true,
lint_cson: true,
lint_javascript: true,
lint_jade: true,
lint_stylus: true,
lint_css: true,
lint_yaml: true,
}; // note 07/09/15 for some reason, not having a coma after the last entry would cause the defaultValues not to be read in the settings editor...
return fTextSettingsResource;
})(SupCore.Data.Base.Resource);
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = fTextSettingsResource;
},{}],6:[function(require,module,exports){
var OT = require("./index");
var Document = (function () {
function Document(text, revisionId) {
if (text === void 0) { text = ""; }
if (revisionId === void 0) { revisionId = 0; }
this.operations = [];
this.text = text;
this._refRevisionId = revisionId;
}
Document.prototype.apply = function (newOperation, revision) {
revision -= this._refRevisionId;
// Should't happen
if (revision > this.operations.length)
throw new Error("The operation base revision is greater than the document revision");
if (revision < this.operations.length) {
// Conflict!
var missedOperations = new OT.TextOperation(this.operations[revision].userId);
missedOperations.targetLength = this.operations[revision].baseLength;
for (var index = revision; index < this.operations.length; index++)
missedOperations = missedOperations.compose(this.operations[index]);
newOperation = missedOperations.transform(newOperation)[1];
}
this.text = newOperation.apply(this.text);
this.operations.push(newOperation.clone());
return newOperation;
};
Document.prototype.getRevisionId = function () { return this.operations.length + this._refRevisionId; };
return Document;
})();
module.exports = Document;
},{"./index":9}],7:[function(require,module,exports){
var TextOp = (function () {
function TextOp(type, attributes) {
this.type = type;
this.attributes = attributes;
}
return TextOp;
})();
module.exports = TextOp;
},{}],8:[function(require,module,exports){
var OT = require("./index");
var TextOperation = (function () {
function TextOperation(userId) {
this.ops = [];
// An operation's baseLength is the length of every string the operation
// can be applied to.
this.baseLength = 0;
// The targetLength is the length of every string that results from applying
// the operation on a valid input string.
this.targetLength = 0;
this.userId = userId;
}
TextOperation.prototype.serialize = function () {
var ops = [];
for (var _i = 0, _a = this.ops; _i < _a.length; _i++) {
var op = _a[_i];
ops.push({ type: op.type, attributes: op.attributes });
}
return { ops: ops, userId: this.userId };
};
TextOperation.prototype.deserialize = function (data) {
if (data == null)
return false;
this.userId = data.userId;
for (var _i = 0, _a = data.ops; _i < _a.length; _i++) {
var op = _a[_i];
switch (op.type) {
case "retain":
this.retain(op.attributes.amount);
break;
case "insert":
this.insert(op.attributes.text);
break;
case "delete":
this.delete(op.attributes.text);
break;
default: return false;
}
}
return true;
};
TextOperation.prototype.retain = function (amount) {
if (typeof (amount) !== "number" || amount <= 0)
return;
this.baseLength += amount;
this.targetLength += amount;
var prevOp = this.ops[this.ops.length - 1];
if (prevOp != null && prevOp.type === "retain") {
prevOp.attributes.amount += amount;
}
else {
this.ops.push(new OT.TextOp("retain", { amount: amount }));
}
};
TextOperation.prototype.insert = function (text) {
if (typeof (text) !== "string" || text === "")
return;
this.targetLength += text.length;
var prevOp = this.ops[this.ops.length - 1];
if (prevOp != null && prevOp.type === "insert") {
prevOp.attributes.text += text;
}
else {
this.ops.push(new OT.TextOp("insert", { text: text }));
}
};
TextOperation.prototype.delete = function (text) {
if (typeof (text) !== "string" || text === "")
return;
this.baseLength += text.length;
var prevOp = this.ops[this.ops.length - 1];
if (prevOp != null && prevOp.type === "delete") {
prevOp.attributes.text += text;
}
else {
this.ops.push(new OT.TextOp("delete", { text: text }));
}
};
TextOperation.prototype.apply = function (text) {
if (text.length !== this.baseLength)
throw new Error("The operation's base length must be equal to the string's length.");
var index = 0;
for (var _i = 0, _a = this.ops; _i < _a.length; _i++) {
var op = _a[_i];
switch (op.type) {
case "retain":
index += op.attributes.amount;
break;
case "insert":
text = text.substring(0, index) + op.attributes.text + text.substring(index, text.length);
index += op.attributes.text.length;
break;
case "delete":
text = text.substring(0, index) + text.substring(index + op.attributes.text.length, text.length);
break;
}
}
return text;
};
TextOperation.prototype.invert = function () {
var invertedOperation = new TextOperation(this.userId);
for (var _i = 0, _a = this.ops; _i < _a.length; _i++) {
var op = _a[_i];
switch (op.type) {
case "retain":
invertedOperation.retain(op.attributes.amount);
break;
case "insert":
invertedOperation.delete(op.attributes.text);
break;
case "delete":
invertedOperation.insert(op.attributes.text);
break;
}
}
return invertedOperation;
};
TextOperation.prototype.clone = function () {
var operation = new TextOperation(this.userId);
for (var _i = 0, _a = this.ops; _i < _a.length; _i++) {
var op = _a[_i];
switch (op.type) {
case "retain":
operation.retain(op.attributes.amount);
break;
case "insert":
operation.insert(op.attributes.text);
break;
case "delete":
operation.delete(op.attributes.text);
break;
}
}
return operation;
};
TextOperation.prototype.equal = function (otherOperation) {
// if (otherOperation.insertedLength !== this.insertedLength) return false;
if (otherOperation.ops.length !== this.ops.length)
return false;
for (var opIndex = 0; opIndex < this.ops.length; opIndex++) {
var op = this.ops[opIndex];
var otherOp = otherOperation.ops[opIndex];
if (otherOp.type !== op.type)
return false;
for (var key in op.attributes) {
var attribute = op.attributes[key];
if (attribute !== otherOp.attributes[key])
return false;
}
}
return true;
};
/*
Largely inspired from Firepad
Compose merges two consecutive operations into one operation, that
preserves the changes of both. Or, in other words, for each input string S
and a pair of consecutive operations A and B,
apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
*/
TextOperation.prototype.compose = function (operation2) {
if (this.targetLength !== operation2.baseLength)
throw new Error("The base length of the second operation has to be the target length of the first operation");
// the combined operation
var composedOperation = new TextOperation(this.userId);
var ops1 = this.clone().ops;
var ops2 = operation2.clone().ops;
var i1 = 0; // current index into ops1 respectively ops2
var i2 = 0;
var op1 = ops1[i1++]; // current ops
var op2 = ops2[i2++];
while (true) {
// Dispatch on the type of op1 and op2
// end condition: both ops1 and ops2 have been processed
if (op1 == null && op2 == null)
break;
if (op2 == null) {
switch (op1.type) {
case "retain":
composedOperation.retain(op1.attributes.amount);
break;
case "insert":
composedOperation.insert(op1.attributes.text);
break;
case "delete":
composedOperation.delete(op1.attributes.text);
break;
}
op1 = ops1[i1++];
continue;
}
if (op1 == null) {
switch (op2.type) {
case "retain":
composedOperation.retain(op2.attributes.amount);
break;
case "insert":
composedOperation.insert(op2.attributes.text);
break;
case "delete":
composedOperation.delete(op2.attributes.text);
break;
}
op2 = ops2[i2++];
continue;
}
if (op1 != null && op1.type === "delete") {
composedOperation.delete(op1.attributes.text);
op1 = ops1[i1++];
continue;
}
if (op2 != null && op2.type === "insert") {
composedOperation.insert(op2.attributes.text);
op2 = ops2[i2++];
continue;
}
if (op1 == null)
throw new Error("Cannot transform operations: first operation is too short.");
if (op2 == null)
throw new Error("Cannot transform operations: first operation is too long.");
if (op1.type === "retain" && op2.type === "retain") {
if (op1.attributes.amount === op2.attributes.amount) {
composedOperation.retain(op1.attributes.amount);
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.amount > op2.attributes.amount) {
composedOperation.retain(op2.attributes.amount);
op1.attributes.amount -= op2.attributes.amount;
op2 = ops2[i2++];
}
else {
composedOperation.retain(op1.attributes.amount);
op2.attributes.amount -= op1.attributes.amount;
op1 = ops1[i1++];
}
}
else if (op1.type === "insert" && op2.type === "delete") {
if (op1.attributes.text.length === op2.attributes.text) {
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.text.length > op2.attributes.text.length) {
op1.attributes.text = op1.attributes.text.slice(op2.attributes.text.length);
op2 = ops2[i2++];
}
else {
op2.attributes.text = op2.attributes.text.slice(op1.attributes.text.length);
op1 = ops1[i1++];
}
}
else if (op1.type === "insert" && op2.type === "retain") {
if (op1.attributes.text.length > op2.attributes.amount) {
composedOperation.insert(op1.attributes.text.slice(0, op2.attributes.amount));
op1.attributes.text = op1.attributes.text.slice(op2.attributes.amount);
op2 = ops2[i2++];
}
else if (op1.attributes.text.length === op2.attributes.amount) {
composedOperation.insert(op1.attributes.text);
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else {
composedOperation.insert(op1.attributes.text);
op2.attributes.amount -= op1.attributes.text.length;
op1 = ops1[i1++];
}
}
else if (op1.type === "retain" && op2.type === "delete") {
if (op1.attributes.amount === op2.attributes.text.length) {
composedOperation.delete(op2.attributes.text);
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.amount > op2.attributes.text.length) {
composedOperation.delete(op2.attributes.text);
op1.attributes.amount -= op2.attributes.text.length;
op2 = ops2[i2++];
}
else {
composedOperation.delete(op2.attributes.text.slice(0, op1.attributes.amount));
op2.attributes.text = op2.attributes.text.slice(op1.attributes.amount);
op1 = ops1[i1++];
}
}
else {
throw new Error("This shouldn't happen: op1: " + JSON.stringify(op1) + ", op2: " + JSON.stringify(op2));
}
}
return composedOperation;
};
/*
Largely inspired from Firepad
Transform takes two operations A (this) and B (other) that happened concurrently and
produces two operations A' and B' (in an array) such that
`apply(apply(S, A), B') = apply(apply(S, B), A')`.
This function is the heart of OT.
*/
TextOperation.prototype.transform = function (operation2) {
var operation1prime, operation2prime;
var ops1, ops2;
// Give priority with the user id
if (this.gotPriority(operation2.userId)) {
operation1prime = new TextOperation(this.userId);
operation2prime = new TextOperation(operation2.userId);
ops1 = this.clone().ops;
ops2 = operation2.clone().ops;
}
else {
operation1prime = new TextOperation(operation2.userId);
operation2prime = new TextOperation(this.userId);
ops1 = operation2.clone().ops;
ops2 = this.clone().ops;
}
var i1 = 0;
var i2 = 0;
var op1 = ops1[i1++];
var op2 = ops2[i2++];
while (true) {
// At every iteration of the loop, the imaginary cursor that both
// operation1 and operation2 have that operates on the input string must
// have the same position in the input string.
// end condition: both ops1 and ops2 have been processed
if (op1 == null && op2 == null)
break;
// next two cases: one or both ops are insert ops
// => insert the string in the corresponding prime operation, skip it in
// the other one. If both op1 and op2 are insert ops, prefer op1.
if (op1 != null && op1.type === "insert") {
operation1prime.insert(op1.attributes.text);
operation2prime.retain(op1.attributes.text.length);
op1 = ops1[i1++];
continue;
}
if (op2 != null && op2.type === "insert") {
operation1prime.retain(op2.attributes.text.length);
operation2prime.insert(op2.attributes.text);
op2 = ops2[i2++];
continue;
}
if (op1 == null)
throw new Error("Cannot transform operations: first operation is too short.");
if (op2 == null)
throw new Error("Cannot transform operations: first operation is too long.");
if (op1.type === "retain" && op2.type === "retain") {
// Simple case: retain/retain
var minl = void 0;
if (op1.attributes.amount === op2.attributes.amount) {
minl = op2.attributes.amount;
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.amount > op2.attributes.amount) {
minl = op2.attributes.amount;
op1.attributes.amount -= op2.attributes.amount;
op2 = ops2[i2++];
}
else {
minl = op1.attributes.amount;
op2.attributes.amount -= op1.attributes.amount;
op1 = ops1[i1++];
}
operation1prime.retain(minl);
operation2prime.retain(minl);
}
else if (op1.type === "delete" && op2.type === "delete") {
// Both operations delete the same string at the same position. We don't
// need to produce any operations, we just skip over the delete ops and
// handle the case that one operation deletes more than the other.
if (op1.attributes.text.length === op2.attributes.text.length) {
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.text.length > op2.attributes.text.length) {
op1.attributes.text = op1.attributes.text.slice(op2.attributes.text.length);
op2 = ops2[i2++];
}
else {
op2.attributes.text = op1.attributes.text.slice(op1.attributes.text.length);
op1 = ops1[i1++];
}
}
else if (op1.type === "delete" && op2.type === "retain") {
var text = void 0;
if (op1.attributes.text.length === op2.attributes.amount) {
text = op1.attributes.text;
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.text.length > op2.attributes.amount) {
text = op1.attributes.text.slice(0, op2.attributes.amount);
op1.attributes.text = op1.attributes.text.slice(op2.attributes.amount);
op2 = ops2[i2++];
}
else {
text = op1.attributes.text;
op2.attributes.amount -= op1.attributes.text.length;
op1 = ops1[i1++];
}
operation1prime.delete(text);
}
else if (op1.type === "retain" && op2.type === "delete") {
var text = void 0;
if (op1.attributes.amount === op2.attributes.text.length) {
text = op2.attributes.text;
op1 = ops1[i1++];
op2 = ops2[i2++];
}
else if (op1.attributes.amount > op2.attributes.text.length) {
text = op2.attributes.text;
op1.attributes.amount -= op2.attributes.text.length;
op2 = ops2[i2++];
}
else {
text = op2.attributes.text.slice(0, op1.attributes.amount);
op2.attributes.text = op2.attributes.text.slice(op1.attributes.amount);
op1 = ops1[i1++];
}
operation2prime.delete(text);
}
else {
throw new Error("The two operations aren't compatible");
}
}
if (this.gotPriority(operation2.userId))
return [operation1prime, operation2prime];
else
return [operation2prime, operation1prime];
};
TextOperation.prototype.gotPriority = function (id2) { return (this.userId <= id2); };
return TextOperation;
})();
module.exports = TextOperation;
},{"./index":9}],9:[function(require,module,exports){
var TextOp = require("./TextOp");
exports.TextOp = TextOp;
var Document = require("./Document");
exports.Document = Document;
var TextOperation = require("./TextOperation");
exports.TextOperation = TextOperation;
},{"./Document":6,"./TextOp":7,"./TextOperation":8}]},{},[3]);