pad.libre-service.eu-etherpad/src/static/js/Changeset.js

2317 lines
65 KiB
JavaScript
Raw Normal View History

2011-05-30 16:53:11 +02:00
/*
* This is the Changeset library copied from the old Etherpad with some modifications to use it in node.js
* Can be found in https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js
*/
2011-05-30 16:53:11 +02:00
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
* This helps other people to understand this code better and helps them to improve it.
* TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED
*/
2011-05-30 16:53:11 +02:00
/*
* Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
2011-03-26 14:10:41 +01:00
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
2020-11-23 19:24:19 +01:00
const AttributePool = require('./AttributePool');
2011-03-26 14:10:41 +01:00
2012-02-26 12:22:46 +01:00
/**
* ==================== General Util Functions =======================
*/
2012-02-26 11:27:56 +01:00
2012-02-26 12:22:46 +01:00
/**
* This method is called whenever there is an error in the sync process
* @param msg {string} Just some message
*/
2011-03-26 14:10:41 +01:00
exports.error = function error(msg) {
2020-11-23 19:24:19 +01:00
const e = new Error(msg);
2011-03-26 14:10:41 +01:00
e.easysync = true;
throw e;
};
2012-02-26 11:27:56 +01:00
2012-02-26 12:22:46 +01:00
/**
* This method is used for assertions with Messages
* if assert fails, the error function is called.
2012-02-26 12:22:46 +01:00
* @param b {boolean} assertion condition
* @param msgParts {string} error to be passed if it fails
*/
2011-03-26 14:10:41 +01:00
exports.assert = function assert(b, msgParts) {
if (!b) {
2020-11-23 19:24:19 +01:00
const msg = Array.prototype.slice.call(arguments, 1).join('');
exports.error(`Failed assertion: ${msg}`);
2011-03-26 14:10:41 +01:00
}
};
2012-02-26 12:22:46 +01:00
/**
* Parses a number from string base 36
* @param str {string} string of the number in base 36
* @returns {int} number
*/
2011-03-26 14:10:41 +01:00
exports.parseNum = function (str) {
return parseInt(str, 36);
};
2012-02-26 11:27:56 +01:00
2012-02-26 12:22:46 +01:00
/**
* Writes a number in base 36 and puts it in a string
* @param num {int} number
* @returns {string} string
*/
2011-03-26 14:10:41 +01:00
exports.numToString = function (num) {
return num.toString(36).toLowerCase();
};
2012-02-26 11:27:56 +01:00
2012-02-26 12:22:46 +01:00
/**
* Converts stuff before $ to base 10
* @obsolete not really used anywhere??
* @param cs {string} the string
* @return integer
2012-02-26 12:22:46 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.toBaseTen = function (cs) {
2020-11-23 19:24:19 +01:00
const dollarIndex = cs.indexOf('$');
const beforeDollar = cs.substring(0, dollarIndex);
const fromDollar = cs.substring(dollarIndex);
return beforeDollar.replace(/[0-9a-z]+/g, (s) => String(exports.parseNum(s))) + fromDollar;
2011-03-26 14:10:41 +01:00
};
2012-02-26 11:27:56 +01:00
2012-02-26 12:22:46 +01:00
/**
* ==================== Changeset Functions =======================
*/
2012-02-26 11:27:56 +01:00
2012-02-26 12:22:46 +01:00
/**
* returns the required length of the text before changeset
2012-02-26 12:22:46 +01:00
* can be applied
* @param cs {string} String representation of the Changeset
*/
2011-03-26 14:10:41 +01:00
exports.oldLen = function (cs) {
return exports.unpack(cs).oldLen;
};
2012-02-26 12:22:46 +01:00
/**
* returns the length of the text after changeset is applied
* @param cs {string} String representation of the Changeset
*/
2011-03-26 14:10:41 +01:00
exports.newLen = function (cs) {
return exports.unpack(cs).newLen;
};
2012-02-26 12:22:46 +01:00
/**
* this function creates an iterator which decodes string changeset operations
* @param opsStr {string} String encoding of the change operations to be performed
* @param optStartIndex {int} from where in the string should the iterator start
* @return {Op} type object iterator
2012-02-26 12:22:46 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.opIterator = function (opsStr, optStartIndex) {
2020-11-23 19:24:19 +01:00
// print(opsStr);
const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g;
const startIndex = (optStartIndex || 0);
let curIndex = startIndex;
let prevIndex = curIndex;
2011-03-26 14:10:41 +01:00
function nextRegexMatch() {
prevIndex = curIndex;
2020-11-23 19:24:19 +01:00
let result;
regex.lastIndex = curIndex;
result = regex.exec(opsStr);
curIndex = regex.lastIndex;
if (result[0] == '?') {
2020-11-23 19:24:19 +01:00
exports.error('Hit error opcode in op stream');
2011-03-26 14:10:41 +01:00
}
2011-03-26 14:10:41 +01:00
return result;
}
2020-11-23 19:24:19 +01:00
let regexResult = nextRegexMatch();
const obj = exports.newOp();
2011-03-26 14:10:41 +01:00
function next(optObj) {
2020-11-23 19:24:19 +01:00
const op = (optObj || obj);
if (regexResult[0]) {
2011-03-26 14:10:41 +01:00
op.attribs = regexResult[1];
op.lines = exports.parseNum(regexResult[2] || 0);
op.opcode = regexResult[3];
op.chars = exports.parseNum(regexResult[4]);
regexResult = nextRegexMatch();
} else {
exports.clearOp(op);
}
return op;
}
function hasNext() {
return !!(regexResult[0]);
2011-03-26 14:10:41 +01:00
}
function lastIndex() {
return prevIndex;
}
return {
2020-11-23 19:24:19 +01:00
next,
hasNext,
lastIndex,
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 12:34:06 +01:00
/**
* Cleans an Op object
* @param {Op} object to be cleared
*/
2011-03-26 14:10:41 +01:00
exports.clearOp = function (op) {
op.opcode = '';
op.chars = 0;
op.lines = 0;
op.attribs = '';
};
2012-02-26 12:34:06 +01:00
/**
* Creates a new Op object
* @param optOpcode the type operation of the Op object
*/
2011-03-26 14:10:41 +01:00
exports.newOp = function (optOpcode) {
return {
opcode: (optOpcode || ''),
chars: 0,
lines: 0,
2020-11-23 19:24:19 +01:00
attribs: '',
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 12:34:06 +01:00
/**
* Clones an Op
* @param op Op to be cloned
*/
2011-03-26 14:10:41 +01:00
exports.cloneOp = function (op) {
return {
opcode: op.opcode,
chars: op.chars,
lines: op.lines,
2020-11-23 19:24:19 +01:00
attribs: op.attribs,
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 12:34:06 +01:00
/**
* Copies op1 to op2
* @param op1 src Op
* @param op2 dest Op
*/
2011-03-26 14:10:41 +01:00
exports.copyOp = function (op1, op2) {
op2.opcode = op1.opcode;
op2.chars = op1.chars;
op2.lines = op1.lines;
op2.attribs = op1.attribs;
};
2012-02-26 12:34:06 +01:00
/**
* Writes the Op in a string the way that changesets need it
*/
2011-03-26 14:10:41 +01:00
exports.opString = function (op) {
// just for debugging
if (!op.opcode) return 'null';
2020-11-23 19:24:19 +01:00
const assem = exports.opAssembler();
2011-03-26 14:10:41 +01:00
assem.append(op);
return assem.toString();
};
2012-02-26 12:34:06 +01:00
/**
* Used just for debugging
*/
2011-03-26 14:10:41 +01:00
exports.stringOp = function (str) {
// just for debugging
return exports.opIterator(str).next();
};
2012-02-26 12:34:06 +01:00
/**
* Used to check if a Changeset if valid
* @param cs {Changeset} Changeset to be checked
*/
2011-03-26 14:10:41 +01:00
exports.checkRep = function (cs) {
// doesn't check things that require access to attrib pool (e.g. attribute order)
// or original string (e.g. newline positions)
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
const oldLen = unpacked.oldLen;
const newLen = unpacked.newLen;
const ops = unpacked.ops;
let charBank = unpacked.charBank;
const assem = exports.smartOpAssembler();
let oldPos = 0;
let calcNewLen = 0;
let numInserted = 0;
const iter = exports.opIterator(ops);
2011-03-26 14:10:41 +01:00
while (iter.hasNext()) {
2020-11-23 19:24:19 +01:00
const o = iter.next();
2011-03-26 14:10:41 +01:00
switch (o.opcode) {
2020-11-23 19:24:19 +01:00
case '=':
oldPos += o.chars;
calcNewLen += o.chars;
break;
case '-':
oldPos += o.chars;
exports.assert(oldPos <= oldLen, oldPos, ' > ', oldLen, ' in ', cs);
break;
case '+':
{
calcNewLen += o.chars;
numInserted += o.chars;
2020-11-23 19:24:19 +01:00
exports.assert(calcNewLen <= newLen, calcNewLen, ' > ', newLen, ' in ', cs);
break;
}
2011-03-26 14:10:41 +01:00
}
assem.append(o);
}
calcNewLen += oldLen - oldPos;
charBank = charBank.substring(0, numInserted);
while (charBank.length < numInserted) {
2020-11-23 19:24:19 +01:00
charBank += '?';
2011-03-26 14:10:41 +01:00
}
assem.endDocument();
2020-11-23 19:24:19 +01:00
const normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank);
exports.assert(normalized == cs, 'Invalid changeset (checkRep failed)');
2011-03-26 14:10:41 +01:00
return cs;
2020-11-23 19:24:19 +01:00
};
2011-03-26 14:10:41 +01:00
2012-02-26 12:34:06 +01:00
/**
2012-02-26 13:18:17 +01:00
* ==================== Util Functions =======================
*/
/**
* creates an object that allows you to append operations (type Op) and also
* compresses them if possible
2012-02-26 12:34:06 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.smartOpAssembler = function () {
// Like opAssembler but able to produce conforming exportss
// from slightly looser input, at the cost of speed.
// Specifically:
// - merges consecutive operations that can be merged
// - strips final "="
// - ignores 0-length changes
// - reorders consecutive + and - (which margingOpAssembler doesn't do)
2020-11-23 19:24:19 +01:00
const minusAssem = exports.mergingOpAssembler();
const plusAssem = exports.mergingOpAssembler();
const keepAssem = exports.mergingOpAssembler();
const assem = exports.stringAssembler();
let lastOpcode = '';
let lengthChange = 0;
2011-03-26 14:10:41 +01:00
function flushKeeps() {
assem.append(keepAssem.toString());
keepAssem.clear();
}
function flushPlusMinus() {
assem.append(minusAssem.toString());
minusAssem.clear();
assem.append(plusAssem.toString());
plusAssem.clear();
}
function append(op) {
if (!op.opcode) return;
if (!op.chars) return;
if (op.opcode == '-') {
if (lastOpcode == '=') {
flushKeeps();
}
minusAssem.append(op);
lengthChange -= op.chars;
} else if (op.opcode == '+') {
if (lastOpcode == '=') {
flushKeeps();
}
plusAssem.append(op);
lengthChange += op.chars;
} else if (op.opcode == '=') {
if (lastOpcode != '=') {
flushPlusMinus();
}
keepAssem.append(op);
}
lastOpcode = op.opcode;
}
function appendOpWithText(opcode, text, attribs, pool) {
2020-11-23 19:24:19 +01:00
const op = exports.newOp(opcode);
2011-03-26 14:10:41 +01:00
op.attribs = exports.makeAttribsString(opcode, attribs, pool);
2020-11-23 19:24:19 +01:00
const lastNewlinePos = text.lastIndexOf('\n');
2011-03-26 14:10:41 +01:00
if (lastNewlinePos < 0) {
op.chars = text.length;
op.lines = 0;
append(op);
} else {
op.chars = lastNewlinePos + 1;
op.lines = text.match(/\n/g).length;
append(op);
op.chars = text.length - (lastNewlinePos + 1);
op.lines = 0;
append(op);
}
}
function toString() {
flushPlusMinus();
flushKeeps();
return assem.toString();
}
function clear() {
minusAssem.clear();
plusAssem.clear();
keepAssem.clear();
assem.clear();
lengthChange = 0;
}
function endDocument() {
keepAssem.endDocument();
}
function getLengthChange() {
return lengthChange;
}
return {
2020-11-23 19:24:19 +01:00
append,
toString,
clear,
endDocument,
appendOpWithText,
getLengthChange,
2011-03-26 14:10:41 +01:00
};
};
exports.mergingOpAssembler = function () {
// This assembler can be used in production; it efficiently
// merges consecutive operations that are mergeable, ignores
// no-ops, and drops final pure "keeps". It does not re-order
// operations.
2020-11-23 19:24:19 +01:00
const assem = exports.opAssembler();
const bufOp = exports.newOp();
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
// This variable stores the length of yyy and any other newline-less
// ops immediately after it.
2020-11-23 19:24:19 +01:00
let bufOpAdditionalCharsAfterNewline = 0;
function flush(isEndDocument) {
if (bufOp.opcode) {
if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) {
// final merged keep, leave it implicit
} else {
assem.append(bufOp);
if (bufOpAdditionalCharsAfterNewline) {
bufOp.chars = bufOpAdditionalCharsAfterNewline;
bufOp.lines = 0;
2011-03-26 14:10:41 +01:00
assem.append(bufOp);
bufOpAdditionalCharsAfterNewline = 0;
2011-03-26 14:10:41 +01:00
}
}
bufOp.opcode = '';
2011-03-26 14:10:41 +01:00
}
}
2011-03-26 14:10:41 +01:00
function append(op) {
if (op.chars > 0) {
if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) {
if (op.lines > 0) {
// bufOp and additional chars are all mergeable into a multi-line op
bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars;
bufOp.lines += op.lines;
bufOpAdditionalCharsAfterNewline = 0;
} else if (bufOp.lines == 0) {
// both bufOp and op are in-line
bufOp.chars += op.chars;
2011-03-26 14:10:41 +01:00
} else {
// append in-line text to multi-line bufOp
bufOpAdditionalCharsAfterNewline += op.chars;
2011-03-26 14:10:41 +01:00
}
} else {
flush();
exports.copyOp(op, bufOp);
2011-03-26 14:10:41 +01:00
}
}
}
2011-03-26 14:10:41 +01:00
function endDocument() {
flush(true);
}
2011-03-26 14:10:41 +01:00
function toString() {
flush();
return assem.toString();
}
2011-03-26 14:10:41 +01:00
function clear() {
assem.clear();
exports.clearOp(bufOp);
}
return {
2020-11-23 19:24:19 +01:00
append,
toString,
clear,
endDocument,
2011-03-26 14:10:41 +01:00
};
};
2011-03-26 14:10:41 +01:00
exports.opAssembler = function () {
2020-11-23 19:24:19 +01:00
const pieces = [];
// this function allows op to be mutated later (doesn't keep a ref)
2011-03-26 14:10:41 +01:00
function append(op) {
pieces.push(op.attribs);
if (op.lines) {
pieces.push('|', exports.numToString(op.lines));
2011-03-26 14:10:41 +01:00
}
pieces.push(op.opcode);
pieces.push(exports.numToString(op.chars));
}
2011-03-26 14:10:41 +01:00
function toString() {
return pieces.join('');
}
2011-03-26 14:10:41 +01:00
function clear() {
pieces.length = 0;
}
return {
2020-11-23 19:24:19 +01:00
append,
toString,
clear,
2011-03-26 14:10:41 +01:00
};
};
2011-03-26 14:10:41 +01:00
2012-02-26 13:18:17 +01:00
/**
* A custom made String Iterator
* @param str {string} String to be iterated over
*/
2011-03-26 14:10:41 +01:00
exports.stringIterator = function (str) {
2020-11-23 19:24:19 +01:00
let curIndex = 0;
// newLines is the number of \n between curIndex and str.length
2020-11-23 19:24:19 +01:00
let newLines = str.split('\n').length - 1;
function getnewLines() {
return newLines;
2014-12-14 17:48:19 +01:00
}
2011-03-26 14:10:41 +01:00
function assertRemaining(n) {
2020-11-23 19:24:19 +01:00
exports.assert(n <= remaining(), '!(', n, ' <= ', remaining(), ')');
2011-03-26 14:10:41 +01:00
}
function take(n) {
assertRemaining(n);
2020-11-23 19:24:19 +01:00
const s = str.substr(curIndex, n);
newLines -= s.split('\n').length - 1;
2011-03-26 14:10:41 +01:00
curIndex += n;
return s;
}
function peek(n) {
assertRemaining(n);
2020-11-23 19:24:19 +01:00
const s = str.substr(curIndex, n);
2011-03-26 14:10:41 +01:00
return s;
}
function skip(n) {
assertRemaining(n);
curIndex += n;
}
function remaining() {
return str.length - curIndex;
}
return {
2020-11-23 19:24:19 +01:00
take,
skip,
remaining,
peek,
newlines: getnewLines,
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 13:18:17 +01:00
/**
* A custom made StringBuffer
2012-02-26 13:18:17 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.stringAssembler = function () {
2020-11-23 19:24:19 +01:00
const pieces = [];
2011-03-26 14:10:41 +01:00
function append(x) {
pieces.push(String(x));
}
function toString() {
return pieces.join('');
}
return {
2020-11-23 19:24:19 +01:00
append,
toString,
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 19:59:11 +01:00
/**
* This class allows to iterate and modify texts which have several lines
* It is used for applying Changesets on arrays of lines
* Note from prev docs: "lines" need not be an array as long as it supports certain calls (lines_foo inside).
*/
2011-03-26 14:10:41 +01:00
exports.textLinesMutator = function (lines) {
// Mutates lines, an array of strings, in place.
// Mutation operations have the same constraints as exports operations
// with respect to newlines, but not the other additional constraints
// (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline).
// Can be used to mutate lists of strings where the last char of each string
// is not actually a newline, but for the purposes of N and L values,
// the caller should pretend it is, and for things to work right in that case, the input
// to insert() should be a single line with no newlines.
2020-11-23 19:24:19 +01:00
const curSplice = [0, 0];
let inSplice = false;
2011-03-26 14:10:41 +01:00
// position in document after curSplice is applied:
2020-11-23 19:24:19 +01:00
let curLine = 0;
let curCol = 0;
2011-03-26 14:10:41 +01:00
// invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
// curLine >= curSplice[0]
// invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
// curCol == 0
function lines_applySplice(s) {
lines.splice.apply(lines, s);
}
function lines_toSource() {
return lines.toSource();
}
function lines_get(idx) {
if (lines.get) {
return lines.get(idx);
} else {
return lines[idx];
}
}
// can be unimplemented if removeLines's return value not needed
function lines_slice(start, end) {
if (lines.slice) {
return lines.slice(start, end);
} else {
return [];
}
}
function lines_length() {
2020-11-23 19:24:19 +01:00
if ((typeof lines.length) === 'number') {
2011-03-26 14:10:41 +01:00
return lines.length;
} else {
return lines.length();
}
}
function enterSplice() {
curSplice[0] = curLine;
curSplice[1] = 0;
if (curCol > 0) {
putCurLineInSplice();
}
inSplice = true;
}
function leaveSplice() {
lines_applySplice(curSplice);
curSplice.length = 2;
curSplice[0] = curSplice[1] = 0;
inSplice = false;
}
function isCurLineInSplice() {
return (curLine - curSplice[0] < (curSplice.length - 2));
}
function debugPrint(typ) {
2020-11-23 19:24:19 +01:00
print(`${typ}: ${curSplice.toSource()} / ${curLine},${curCol} / ${lines_toSource()}`);
2011-03-26 14:10:41 +01:00
}
function putCurLineInSplice() {
if (!isCurLineInSplice()) {
curSplice.push(lines_get(curSplice[0] + curSplice[1]));
curSplice[1]++;
}
return 2 + curLine - curSplice[0];
}
function skipLines(L, includeInSplice) {
if (L) {
if (includeInSplice) {
if (!inSplice) {
enterSplice();
}
2020-11-23 19:24:19 +01:00
for (let i = 0; i < L; i++) {
2011-03-26 14:10:41 +01:00
curCol = 0;
putCurLineInSplice();
curLine++;
}
} else {
if (inSplice) {
if (L > 1) {
leaveSplice();
} else {
putCurLineInSplice();
}
}
curLine += L;
curCol = 0;
}
2020-11-23 19:24:19 +01:00
// print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length);
/* if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) {
print("BLAH");
putCurLineInSplice();
}*/
2011-03-26 14:10:41 +01:00
// tests case foo in remove(), which isn't otherwise covered in current impl
}
2020-11-23 19:24:19 +01:00
// debugPrint("skip");
2011-03-26 14:10:41 +01:00
}
function skip(N, L, includeInSplice) {
if (N) {
if (L) {
skipLines(L, includeInSplice);
} else {
if (includeInSplice && !inSplice) {
enterSplice();
}
if (inSplice) {
putCurLineInSplice();
}
curCol += N;
2020-11-23 19:24:19 +01:00
// debugPrint("skip");
2011-03-26 14:10:41 +01:00
}
}
}
function removeLines(L) {
2020-11-23 19:24:19 +01:00
let removed = '';
2011-03-26 14:10:41 +01:00
if (L) {
if (!inSplice) {
enterSplice();
}
function nextKLinesText(k) {
2020-11-23 19:24:19 +01:00
const m = curSplice[0] + curSplice[1];
2011-03-26 14:10:41 +01:00
return lines_slice(m, m + k).join('');
}
if (isCurLineInSplice()) {
2020-11-23 19:24:19 +01:00
// print(curCol);
2011-03-26 14:10:41 +01:00
if (curCol == 0) {
removed = curSplice[curSplice.length - 1];
// print("FOO"); // case foo
curSplice.length--;
removed += nextKLinesText(L - 1);
curSplice[1] += L - 1;
} else {
removed = nextKLinesText(L - 1);
curSplice[1] += L - 1;
2020-11-23 19:24:19 +01:00
const sline = curSplice.length - 1;
2011-03-26 14:10:41 +01:00
removed = curSplice[sline].substring(curCol) + removed;
curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]);
curSplice[1] += 1;
}
} else {
removed = nextKLinesText(L);
curSplice[1] += L;
}
2020-11-23 19:24:19 +01:00
// debugPrint("remove");
2011-03-26 14:10:41 +01:00
}
return removed;
}
function remove(N, L) {
2020-11-23 19:24:19 +01:00
let removed = '';
2011-03-26 14:10:41 +01:00
if (N) {
if (L) {
return removeLines(L);
} else {
if (!inSplice) {
enterSplice();
}
2020-11-23 19:24:19 +01:00
const sline = putCurLineInSplice();
2011-03-26 14:10:41 +01:00
removed = curSplice[sline].substring(curCol, curCol + N);
curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N);
2020-11-23 19:24:19 +01:00
// debugPrint("remove");
2011-03-26 14:10:41 +01:00
}
}
return removed;
}
function insert(text, L) {
if (text) {
if (!inSplice) {
enterSplice();
}
if (L) {
2020-11-23 19:24:19 +01:00
const newLines = exports.splitTextLines(text);
2011-03-26 14:10:41 +01:00
if (isCurLineInSplice()) {
2020-11-23 19:24:19 +01:00
// if (curCol == 0) {
// curSplice.length--;
// curSplice[1]--;
// Array.prototype.push.apply(curSplice, newLines);
// curLine += newLines.length;
// }
// else {
2011-03-26 14:10:41 +01:00
var sline = curSplice.length - 1;
2020-11-23 19:24:19 +01:00
const theLine = curSplice[sline];
const lineCol = curCol;
2011-03-26 14:10:41 +01:00
curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
curLine++;
newLines.splice(0, 1);
Array.prototype.push.apply(curSplice, newLines);
curLine += newLines.length;
curSplice.push(theLine.substring(lineCol));
curCol = 0;
2020-11-23 19:24:19 +01:00
// }
2011-03-26 14:10:41 +01:00
} else {
Array.prototype.push.apply(curSplice, newLines);
curLine += newLines.length;
}
} else {
var sline = putCurLineInSplice();
if (!curSplice[sline]) {
2020-11-23 19:24:19 +01:00
console.error('curSplice[sline] not populated, actual curSplice contents is ', curSplice, '. Possibly related to https://github.com/ether/etherpad-lite/issues/2802');
}
2011-03-26 14:10:41 +01:00
curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol);
curCol += text.length;
}
2020-11-23 19:24:19 +01:00
// debugPrint("insert");
2011-03-26 14:10:41 +01:00
}
}
function hasMore() {
2020-11-23 19:24:19 +01:00
// print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]);
let docLines = lines_length();
2011-03-26 14:10:41 +01:00
if (inSplice) {
docLines += curSplice.length - 2 - curSplice[1];
}
return curLine < docLines;
}
function close() {
if (inSplice) {
leaveSplice();
}
2020-11-23 19:24:19 +01:00
// debugPrint("close");
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const self = {
skip,
remove,
insert,
close,
hasMore,
removeLines,
skipLines,
2011-03-26 14:10:41 +01:00
};
return self;
};
2012-02-26 19:59:11 +01:00
/**
* Function allowing iterating over two Op strings.
2012-02-26 19:59:11 +01:00
* @params in1 {string} first Op string
* @params idx1 {int} integer where 1st iterator should start
* @params in2 {string} second Op string
* @params idx2 {int} integer where 2nd iterator should start
* @params func {function} which decides how 1st or 2nd iterator
2012-02-26 19:59:11 +01:00
* advances. When opX.opcode = 0, iterator X advances to
* next element
* func has signature f(op1, op2, opOut)
* op1 - current operation of the first iterator
* op2 - current operation of the second iterator
* opOut - result operator to be put into Changeset
* @return {string} the integrated changeset
*/
2011-03-26 14:10:41 +01:00
exports.applyZip = function (in1, idx1, in2, idx2, func) {
2020-11-23 19:24:19 +01:00
const iter1 = exports.opIterator(in1, idx1);
const iter2 = exports.opIterator(in2, idx2);
const assem = exports.smartOpAssembler();
const op1 = exports.newOp();
const op2 = exports.newOp();
const opOut = exports.newOp();
2011-03-26 14:10:41 +01:00
while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) {
if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1);
if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2);
func(op1, op2, opOut);
if (opOut.opcode) {
2020-11-23 19:24:19 +01:00
// print(opOut.toSource());
2011-03-26 14:10:41 +01:00
assem.append(opOut);
opOut.opcode = '';
}
}
assem.endDocument();
return assem.toString();
};
2012-02-26 13:18:17 +01:00
/**
* Unpacks a string encoded Changeset into a proper Changeset object
* @params cs {string} String encoded Changeset
* @returns {Changeset} a Changeset class
*/
2011-03-26 14:10:41 +01:00
exports.unpack = function (cs) {
2020-11-23 19:24:19 +01:00
const headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
const headerMatch = headerRegex.exec(cs);
2011-03-26 14:10:41 +01:00
if ((!headerMatch) || (!headerMatch[0])) {
2020-11-23 19:24:19 +01:00
exports.error(`Not a exports: ${cs}`);
}
const oldLen = exports.parseNum(headerMatch[1]);
const changeSign = (headerMatch[2] == '>') ? 1 : -1;
const changeMag = exports.parseNum(headerMatch[3]);
const newLen = oldLen + changeSign * changeMag;
const opsStart = headerMatch[0].length;
let opsEnd = cs.indexOf('$');
2011-03-26 14:10:41 +01:00
if (opsEnd < 0) opsEnd = cs.length;
return {
2020-11-23 19:24:19 +01:00
oldLen,
newLen,
2011-03-26 14:10:41 +01:00
ops: cs.substring(opsStart, opsEnd),
2020-11-23 19:24:19 +01:00
charBank: cs.substring(opsEnd + 1),
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 13:18:17 +01:00
/**
* Packs Changeset object into a string
2012-02-26 13:18:17 +01:00
* @params oldLen {int} Old length of the Changeset
* @params newLen {int] New length of the Changeset
* @params opsStr {string} String encoding of the changes to be made
* @params bank {string} Charbank of the Changeset
* @returns {Changeset} a Changeset class
*/
2011-03-26 14:10:41 +01:00
exports.pack = function (oldLen, newLen, opsStr, bank) {
2020-11-23 19:24:19 +01:00
const lenDiff = newLen - oldLen;
const lenDiffStr = (lenDiff >= 0 ? `>${exports.numToString(lenDiff)}` : `<${exports.numToString(-lenDiff)}`);
const a = [];
2011-03-26 14:10:41 +01:00
a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank);
return a.join('');
};
2012-02-26 13:18:17 +01:00
/**
* Applies a Changeset to a string
* @params cs {string} String encoded Changeset
* @params str {string} String to which a Changeset should be applied
*/
2011-03-26 14:10:41 +01:00
exports.applyToText = function (cs, str) {
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
exports.assert(str.length == unpacked.oldLen, 'mismatched apply: ', str.length, ' / ', unpacked.oldLen);
const csIter = exports.opIterator(unpacked.ops);
const bankIter = exports.stringIterator(unpacked.charBank);
const strIter = exports.stringIterator(str);
const assem = exports.stringAssembler();
2011-03-26 14:10:41 +01:00
while (csIter.hasNext()) {
2020-11-23 19:24:19 +01:00
const op = csIter.next();
2011-03-26 14:10:41 +01:00
switch (op.opcode) {
2020-11-23 19:24:19 +01:00
case '+':
// op is + and op.lines 0: no newlines must be in op.chars
// op is + and op.lines >0: op.chars must include op.lines newlines
if (op.lines != bankIter.peek(op.chars).split('\n').length - 1) {
throw new Error(`newline count is wrong in op +; cs:${cs} and text:${str}`);
}
assem.append(bankIter.take(op.chars));
break;
case '-':
// op is - and op.lines 0: no newlines must be in the deleted string
// op is - and op.lines >0: op.lines newlines must be in the deleted string
if (op.lines != strIter.peek(op.chars).split('\n').length - 1) {
throw new Error(`newline count is wrong in op -; cs:${cs} and text:${str}`);
}
strIter.skip(op.chars);
break;
case '=':
// op is = and op.lines 0: no newlines must be in the copied string
// op is = and op.lines >0: op.lines newlines must be in the copied string
if (op.lines != strIter.peek(op.chars).split('\n').length - 1) {
throw new Error(`newline count is wrong in op =; cs:${cs} and text:${str}`);
}
assem.append(strIter.take(op.chars));
break;
2011-03-26 14:10:41 +01:00
}
}
assem.append(strIter.take(strIter.remaining()));
return assem.toString();
2011-03-26 14:10:41 +01:00
};
2012-02-26 19:59:11 +01:00
/**
* applies a changeset on an array of lines
* @param CS {Changeset} the changeset to be applied
* @param lines The lines to which the changeset needs to be applied
*/
2011-03-26 14:10:41 +01:00
exports.mutateTextLines = function (cs, lines) {
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
const csIter = exports.opIterator(unpacked.ops);
const bankIter = exports.stringIterator(unpacked.charBank);
const mut = exports.textLinesMutator(lines);
2011-03-26 14:10:41 +01:00
while (csIter.hasNext()) {
2020-11-23 19:24:19 +01:00
const op = csIter.next();
2011-03-26 14:10:41 +01:00
switch (op.opcode) {
2020-11-23 19:24:19 +01:00
case '+':
mut.insert(bankIter.take(op.chars), op.lines);
break;
case '-':
mut.remove(op.chars, op.lines);
break;
case '=':
mut.skip(op.chars, op.lines, (!!op.attribs));
break;
2011-03-26 14:10:41 +01:00
}
}
mut.close();
};
2012-02-26 19:59:11 +01:00
/**
* Composes two attribute strings (see below) into one.
* @param att1 {string} first attribute string
* @param att2 {string} second attribue string
* @param resultIsMutaton {boolean}
* @param pool {AttribPool} attribute pool
2012-02-26 19:59:11 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.composeAttributes = function (att1, att2, resultIsMutation, pool) {
// att1 and att2 are strings like "*3*f*1c", asMutation is a boolean.
// Sometimes attribute (key,value) pairs are treated as attribute presence
// information, while other times they are treated as operations that
// mutate a set of attributes, and this affects whether an empty value
// is a deletion or a change.
// Examples, of the form (att1Items, att2Items, resultIsMutation) -> result
// ([], [(bold, )], true) -> [(bold, )]
// ([], [(bold, )], false) -> []
// ([], [(bold, true)], true) -> [(bold, true)]
// ([], [(bold, true)], false) -> [(bold, true)]
// ([(bold, true)], [(bold, )], true) -> [(bold, )]
// ([(bold, true)], [(bold, )], false) -> []
// pool can be null if att2 has no attributes.
if ((!att1) && resultIsMutation) {
// In the case of a mutation (i.e. composing two exportss),
// an att2 composed with an empy att1 is just att2. If att1
// is part of an attribution string, then att2 may remove
// attributes that are already gone, so don't do this optimization.
return att2;
}
if (!att2) return att1;
2020-11-23 19:24:19 +01:00
const atts = [];
att1.replace(/\*([0-9a-z]+)/g, (_, a) => {
2011-03-26 14:10:41 +01:00
atts.push(pool.getAttrib(exports.parseNum(a)));
return '';
});
2020-11-23 19:24:19 +01:00
att2.replace(/\*([0-9a-z]+)/g, (_, a) => {
const pair = pool.getAttrib(exports.parseNum(a));
let found = false;
for (let i = 0; i < atts.length; i++) {
const oldPair = atts[i];
2011-03-26 14:10:41 +01:00
if (oldPair[0] == pair[0]) {
if (pair[1] || resultIsMutation) {
oldPair[1] = pair[1];
} else {
atts.splice(i, 1);
}
found = true;
break;
}
}
if ((!found) && (pair[1] || resultIsMutation)) {
atts.push(pair);
}
return '';
});
atts.sort();
2020-11-23 19:24:19 +01:00
const buf = exports.stringAssembler();
for (let i = 0; i < atts.length; i++) {
2011-03-26 14:10:41 +01:00
buf.append('*');
buf.append(exports.numToString(pool.putAttrib(atts[i])));
}
2020-11-23 19:24:19 +01:00
// print(att1+" / "+att2+" / "+buf.toString());
2011-03-26 14:10:41 +01:00
return buf.toString();
};
2012-02-26 19:59:11 +01:00
/**
* Function used as parameter for applyZip to apply a Changeset to an
* attribute
2012-02-26 19:59:11 +01:00
*/
2011-03-26 14:10:41 +01:00
exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) {
// attOp is the op from the sequence that is being operated on, either an
// attribution string or the earlier of two exportss being composed.
// pool can be null if definitely not needed.
2020-11-23 19:24:19 +01:00
// print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
2011-03-26 14:10:41 +01:00
if (attOp.opcode == '-') {
exports.copyOp(attOp, opOut);
attOp.opcode = '';
} else if (!attOp.opcode) {
exports.copyOp(csOp, opOut);
csOp.opcode = '';
} else {
switch (csOp.opcode) {
2020-11-23 19:24:19 +01:00
case '-':
2011-03-26 14:10:41 +01:00
{
if (csOp.chars <= attOp.chars) {
// delete or delete part
if (attOp.opcode == '=') {
opOut.opcode = '-';
opOut.chars = csOp.chars;
opOut.lines = csOp.lines;
opOut.attribs = '';
}
attOp.chars -= csOp.chars;
attOp.lines -= csOp.lines;
csOp.opcode = '';
if (!attOp.chars) {
attOp.opcode = '';
}
} else {
// delete and keep going
if (attOp.opcode == '=') {
opOut.opcode = '-';
opOut.chars = attOp.chars;
opOut.lines = attOp.lines;
opOut.attribs = '';
}
csOp.chars -= attOp.chars;
csOp.lines -= attOp.lines;
attOp.opcode = '';
}
break;
}
2020-11-23 19:24:19 +01:00
case '+':
2011-03-26 14:10:41 +01:00
{
// insert
exports.copyOp(csOp, opOut);
csOp.opcode = '';
break;
}
2020-11-23 19:24:19 +01:00
case '=':
2011-03-26 14:10:41 +01:00
{
if (csOp.chars <= attOp.chars) {
// keep or keep part
opOut.opcode = attOp.opcode;
opOut.chars = csOp.chars;
opOut.lines = csOp.lines;
opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
csOp.opcode = '';
attOp.chars -= csOp.chars;
attOp.lines -= csOp.lines;
if (!attOp.chars) {
attOp.opcode = '';
}
} else {
// keep and keep going
opOut.opcode = attOp.opcode;
opOut.chars = attOp.chars;
opOut.lines = attOp.lines;
opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
attOp.opcode = '';
csOp.chars -= attOp.chars;
csOp.lines -= attOp.lines;
}
break;
}
2020-11-23 19:24:19 +01:00
case '':
2011-03-26 14:10:41 +01:00
{
exports.copyOp(attOp, opOut);
attOp.opcode = '';
break;
}
}
}
};
2012-02-26 19:59:11 +01:00
/**
* Applies a Changeset to the attribs string of a AText.
* @param cs {string} Changeset
* @param astr {string} the attribs string of a AText
* @param pool {AttribsPool} the attibutes pool
*/
2011-03-26 14:10:41 +01:00
exports.applyToAttribution = function (cs, astr, pool) {
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
return exports.applyZip(astr, 0, unpacked.ops, 0, (op1, op2, opOut) => exports._slicerZipperFunc(op1, op2, opOut, pool));
2011-03-26 14:10:41 +01:00
};
2020-11-23 19:24:19 +01:00
/* exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) {
2011-03-26 14:10:41 +01:00
var iter = exports.opIterator(opsStr, optStartIndex);
var bankIndex = 0;
};*/
exports.mutateAttributionLines = function (cs, lines, pool) {
2020-11-23 19:24:19 +01:00
// dmesg(cs);
// dmesg(lines.toSource()+" ->");
const unpacked = exports.unpack(cs);
const csIter = exports.opIterator(unpacked.ops);
const csBank = unpacked.charBank;
let csBankIndex = 0;
2011-03-26 14:10:41 +01:00
// treat the attribution lines as text lines, mutating a line at a time
2020-11-23 19:24:19 +01:00
const mut = exports.textLinesMutator(lines);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let lineIter = null;
2011-03-26 14:10:41 +01:00
function isNextMutOp() {
return (lineIter && lineIter.hasNext()) || mut.hasMore();
}
function nextMutOp(destOp) {
if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) {
2020-11-23 19:24:19 +01:00
const line = mut.removeLines(1);
2011-03-26 14:10:41 +01:00
lineIter = exports.opIterator(line);
}
if (lineIter && lineIter.hasNext()) {
lineIter.next(destOp);
} else {
destOp.opcode = '';
}
}
2020-11-23 19:24:19 +01:00
let lineAssem = null;
2011-03-26 14:10:41 +01:00
function outputMutOp(op) {
2020-11-23 19:24:19 +01:00
// print("outputMutOp: "+op.toSource());
2011-03-26 14:10:41 +01:00
if (!lineAssem) {
lineAssem = exports.mergingOpAssembler();
}
lineAssem.append(op);
if (op.lines > 0) {
2020-11-23 19:24:19 +01:00
exports.assert(op.lines == 1, "Can't have op.lines of ", op.lines, ' in attribution lines');
2011-03-26 14:10:41 +01:00
// ship it to the mut
mut.insert(lineAssem.toString(), 1);
lineAssem = null;
}
}
2020-11-23 19:24:19 +01:00
const csOp = exports.newOp();
const attOp = exports.newOp();
const opOut = exports.newOp();
2011-03-26 14:10:41 +01:00
while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) {
if ((!csOp.opcode) && csIter.hasNext()) {
csIter.next(csOp);
}
2020-11-23 19:24:19 +01:00
// print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
// print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null));
// print("csOp: "+csOp.toSource());
2011-03-26 14:10:41 +01:00
if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) {
break; // done
} else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) {
// skip multiple lines; this is what makes small changes not order of the document size
mut.skipLines(csOp.lines);
2020-11-23 19:24:19 +01:00
// print("skipped: "+csOp.lines);
2011-03-26 14:10:41 +01:00
csOp.opcode = '';
} else if (csOp.opcode == '+') {
if (csOp.lines > 1) {
2020-11-23 19:24:19 +01:00
const firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex;
2011-03-26 14:10:41 +01:00
exports.copyOp(csOp, opOut);
csOp.chars -= firstLineLen;
csOp.lines--;
opOut.lines = 1;
opOut.chars = firstLineLen;
} else {
exports.copyOp(csOp, opOut);
csOp.opcode = '';
}
outputMutOp(opOut);
csBankIndex += opOut.chars;
opOut.opcode = '';
} else {
if ((!attOp.opcode) && isNextMutOp()) {
nextMutOp(attOp);
}
2020-11-23 19:24:19 +01:00
// print("attOp: "+attOp.toSource());
2011-03-26 14:10:41 +01:00
exports._slicerZipperFunc(attOp, csOp, opOut, pool);
if (opOut.opcode) {
outputMutOp(opOut);
opOut.opcode = '';
}
}
}
2020-11-23 19:24:19 +01:00
exports.assert(!lineAssem, `line assembler not finished:${cs}`);
2011-03-26 14:10:41 +01:00
mut.close();
2020-11-23 19:24:19 +01:00
// dmesg("-> "+lines.toSource());
2011-03-26 14:10:41 +01:00
};
2012-02-26 19:59:11 +01:00
/**
* joins several Attribution lines
* @param theAlines collection of Attribution lines
* @returns {string} joined Attribution lines
*/
2011-03-26 14:10:41 +01:00
exports.joinAttributionLines = function (theAlines) {
2020-11-23 19:24:19 +01:00
const assem = exports.mergingOpAssembler();
for (let i = 0; i < theAlines.length; i++) {
const aline = theAlines[i];
const iter = exports.opIterator(aline);
2011-03-26 14:10:41 +01:00
while (iter.hasNext()) {
assem.append(iter.next());
}
}
return assem.toString();
};
exports.splitAttributionLines = function (attrOps, text) {
2020-11-23 19:24:19 +01:00
const iter = exports.opIterator(attrOps);
const assem = exports.mergingOpAssembler();
const lines = [];
let pos = 0;
2011-03-26 14:10:41 +01:00
function appendOp(op) {
assem.append(op);
if (op.lines > 0) {
lines.push(assem.toString());
assem.clear();
}
pos += op.chars;
}
while (iter.hasNext()) {
2020-11-23 19:24:19 +01:00
const op = iter.next();
let numChars = op.chars;
let numLines = op.lines;
2011-03-26 14:10:41 +01:00
while (numLines > 1) {
2020-11-23 19:24:19 +01:00
const newlineEnd = text.indexOf('\n', pos) + 1;
exports.assert(newlineEnd > 0, 'newlineEnd <= 0 in splitAttributionLines');
2011-03-26 14:10:41 +01:00
op.chars = newlineEnd - pos;
op.lines = 1;
appendOp(op);
numChars -= op.chars;
numLines -= op.lines;
}
if (numLines == 1) {
op.chars = numChars;
op.lines = 1;
}
appendOp(op);
}
return lines;
};
2012-02-26 19:59:11 +01:00
/**
* splits text into lines
* @param {string} text to be splitted
*/
2011-03-26 14:10:41 +01:00
exports.splitTextLines = function (text) {
return text.match(/[^\n]*(?:\n|[^\n]$)/g);
};
2012-02-26 19:59:11 +01:00
/**
* compose two Changesets
* @param cs1 {Changeset} first Changeset
* @param cs2 {Changeset} second Changeset
* @param pool {AtribsPool} Attribs pool
*/
2011-03-26 14:10:41 +01:00
exports.compose = function (cs1, cs2, pool) {
2020-11-23 19:24:19 +01:00
const unpacked1 = exports.unpack(cs1);
const unpacked2 = exports.unpack(cs2);
const len1 = unpacked1.oldLen;
const len2 = unpacked1.newLen;
exports.assert(len2 == unpacked2.oldLen, 'mismatched composition of two changesets');
const len3 = unpacked2.newLen;
const bankIter1 = exports.stringIterator(unpacked1.charBank);
const bankIter2 = exports.stringIterator(unpacked2.charBank);
const bankAssem = exports.stringAssembler();
const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => {
// var debugBuilder = exports.stringAssembler();
// debugBuilder.append(exports.opString(op1));
// debugBuilder.append(',');
// debugBuilder.append(exports.opString(op2));
// debugBuilder.append(' / ');
const op1code = op1.opcode;
const op2code = op2.opcode;
2011-03-26 14:10:41 +01:00
if (op1code == '+' && op2code == '-') {
bankIter1.skip(Math.min(op1.chars, op2.chars));
}
exports._slicerZipperFunc(op1, op2, opOut, pool);
if (opOut.opcode == '+') {
if (op2code == '+') {
bankAssem.append(bankIter2.take(opOut.chars));
} else {
bankAssem.append(bankIter1.take(opOut.chars));
}
}
2020-11-23 19:24:19 +01:00
// debugBuilder.append(exports.opString(op1));
// debugBuilder.append(',');
// debugBuilder.append(exports.opString(op2));
// debugBuilder.append(' -> ');
// debugBuilder.append(exports.opString(opOut));
// print(debugBuilder.toString());
2011-03-26 14:10:41 +01:00
});
return exports.pack(len1, len3, newOps, bankAssem.toString());
};
2012-02-26 19:59:11 +01:00
/**
* returns a function that tests if a string of attributes
* (e.g. *3*4) contains a given attribute key,value that
* is already present in the pool.
* @param attribPair array [key,value] of the attribute
2012-02-26 19:59:11 +01:00
* @param pool {AttribPool} Attribute pool
*/
2011-03-26 14:10:41 +01:00
exports.attributeTester = function (attribPair, pool) {
if (!pool) {
return never;
}
2020-11-23 19:24:19 +01:00
const attribNum = pool.putAttrib(attribPair, true);
2011-03-26 14:10:41 +01:00
if (attribNum < 0) {
return never;
} else {
2020-11-23 19:24:19 +01:00
const re = new RegExp(`\\*${exports.numToString(attribNum)}(?!\\w)`);
2011-03-26 14:10:41 +01:00
return function (attribs) {
return re.test(attribs);
};
}
function never(attribs) {
return false;
}
};
2012-02-26 19:59:11 +01:00
/**
* creates the identity Changeset of length N
* @param N {int} length of the identity changeset
*/
2011-03-26 14:10:41 +01:00
exports.identity = function (N) {
2020-11-23 19:24:19 +01:00
return exports.pack(N, N, '', '');
2011-03-26 14:10:41 +01:00
};
2012-02-26 19:59:11 +01:00
/**
* creates a Changeset which works on oldFullText and removes text
* from spliceStart to spliceStart+numRemoved and inserts newText
* instead. Also gives possibility to add attributes optNewTextAPairs
2012-02-26 19:59:11 +01:00
* for the new text.
* @param oldFullText {string} old text
* @param spliecStart {int} where splicing starts
* @param numRemoved {int} number of characters to be removed
* @param newText {string} string to be inserted
* @param optNewTextAPairs {string} new pairs to be inserted
* @param pool {AttribPool} Attribution Pool
*/
2011-03-26 14:10:41 +01:00
exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) {
2020-11-23 19:24:19 +01:00
const oldLen = oldFullText.length;
2011-03-26 14:10:41 +01:00
if (spliceStart >= oldLen) {
spliceStart = oldLen - 1;
}
if (numRemoved > oldFullText.length - spliceStart) {
numRemoved = oldFullText.length - spliceStart;
2011-03-26 14:10:41 +01:00
}
2020-11-23 19:24:19 +01:00
const oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved);
const newLen = oldLen + newText.length - oldText.length;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const assem = exports.smartOpAssembler();
2011-03-26 14:10:41 +01:00
assem.appendOpWithText('=', oldFullText.substring(0, spliceStart));
assem.appendOpWithText('-', oldText);
assem.appendOpWithText('+', newText, optNewTextAPairs, pool);
assem.endDocument();
return exports.pack(oldLen, newLen, assem.toString(), newText);
};
2012-02-26 19:59:11 +01:00
/**
* Transforms a changeset into a list of splices in the form
* [startChar, endChar, newText] meaning replace text from
* startChar to endChar with newText
* @param cs Changeset
*/
2011-03-26 14:10:41 +01:00
exports.toSplices = function (cs) {
//
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
const splices = [];
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
let oldPos = 0;
const iter = exports.opIterator(unpacked.ops);
const charIter = exports.stringIterator(unpacked.charBank);
let inSplice = false;
2011-03-26 14:10:41 +01:00
while (iter.hasNext()) {
2020-11-23 19:24:19 +01:00
const op = iter.next();
2011-03-26 14:10:41 +01:00
if (op.opcode == '=') {
oldPos += op.chars;
inSplice = false;
} else {
if (!inSplice) {
2020-11-23 19:24:19 +01:00
splices.push([oldPos, oldPos, '']);
2011-03-26 14:10:41 +01:00
inSplice = true;
}
if (op.opcode == '-') {
oldPos += op.chars;
splices[splices.length - 1][1] += op.chars;
} else if (op.opcode == '+') {
splices[splices.length - 1][2] += charIter.take(op.chars);
}
}
}
return splices;
};
2012-02-26 19:59:11 +01:00
/**
*
2012-02-26 19:59:11 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) {
2020-11-23 19:24:19 +01:00
let newStartChar = startChar;
let newEndChar = endChar;
const splices = exports.toSplices(cs);
let lengthChangeSoFar = 0;
for (let i = 0; i < splices.length; i++) {
const splice = splices[i];
const spliceStart = splice[0] + lengthChangeSoFar;
const spliceEnd = splice[1] + lengthChangeSoFar;
const newTextLength = splice[2].length;
const thisLengthChange = newTextLength - (spliceEnd - spliceStart);
2011-03-26 14:10:41 +01:00
if (spliceStart <= newStartChar && spliceEnd >= newEndChar) {
// splice fully replaces/deletes range
// (also case that handles insertion at a collapsed selection)
if (insertionsAfter) {
newStartChar = newEndChar = spliceStart;
} else {
newStartChar = newEndChar = spliceStart + newTextLength;
}
} else if (spliceEnd <= newStartChar) {
// splice is before range
newStartChar += thisLengthChange;
newEndChar += thisLengthChange;
} else if (spliceStart >= newEndChar) {
// splice is after range
} else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) {
// splice is inside range
newEndChar += thisLengthChange;
} else if (spliceEnd < newEndChar) {
// splice overlaps beginning of range
newStartChar = spliceStart + newTextLength;
newEndChar += thisLengthChange;
} else {
// splice overlaps end of range
newEndChar = spliceStart;
}
lengthChangeSoFar += thisLengthChange;
}
return [newStartChar, newEndChar];
};
2012-02-26 19:59:11 +01:00
/**
* Iterate over attributes in a changeset and move them from
* oldPool to newPool
* @param cs {Changeset} Chageset/attribution string to be iterated over
* @param oldPool {AttribPool} old attributes pool
* @param newPool {AttribPool} new attributes pool
* @return {string} the new Changeset
*/
2011-03-26 14:10:41 +01:00
exports.moveOpsToNewPool = function (cs, oldPool, newPool) {
// works on exports or attribution string
2020-11-23 19:24:19 +01:00
let dollarPos = cs.indexOf('$');
2011-03-26 14:10:41 +01:00
if (dollarPos < 0) {
dollarPos = cs.length;
}
2020-11-23 19:24:19 +01:00
const upToDollar = cs.substring(0, dollarPos);
const fromDollar = cs.substring(dollarPos);
2011-03-26 14:10:41 +01:00
// order of attribs stays the same
2020-11-23 19:24:19 +01:00
return upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
const oldNum = exports.parseNum(a);
let pair = oldPool.getAttrib(oldNum);
/*
* Setting an empty pair. Required for when delete pad contents / attributes
* while another user has the timeslider open.
*
* Fixes https://github.com/ether/etherpad-lite/issues/3932
*/
if (!pair) {
pair = [];
}
2020-11-23 19:24:19 +01:00
const newNum = newPool.putAttrib(pair);
return `*${exports.numToString(newNum)}`;
2011-03-26 14:10:41 +01:00
}) + fromDollar;
};
2012-02-26 19:59:11 +01:00
/**
* create an attribution inserting a text
* @param text {string} text to be inserted
*/
2011-03-26 14:10:41 +01:00
exports.makeAttribution = function (text) {
2020-11-23 19:24:19 +01:00
const assem = exports.smartOpAssembler();
2011-03-26 14:10:41 +01:00
assem.appendOpWithText('+', text);
return assem.toString();
};
2012-02-26 19:59:11 +01:00
/**
* Iterates over attributes in exports, attribution string, or attribs property of an op
* and runs function func on them
* @param cs {Changeset} changeset
* @param func {function} function to be called
*/
2011-03-26 14:10:41 +01:00
exports.eachAttribNumber = function (cs, func) {
2020-11-23 19:24:19 +01:00
let dollarPos = cs.indexOf('$');
2011-03-26 14:10:41 +01:00
if (dollarPos < 0) {
dollarPos = cs.length;
}
2020-11-23 19:24:19 +01:00
const upToDollar = cs.substring(0, dollarPos);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
upToDollar.replace(/\*([0-9a-z]+)/g, (_, a) => {
2011-03-26 14:10:41 +01:00
func(exports.parseNum(a));
return '';
});
};
2012-02-26 19:59:11 +01:00
/**
* Filter attributes which should remain in a Changeset
* callable on a exports, attribution string, or attribs property of an op,
* though it may easily create adjacent ops that can be merged.
* @param cs {Changeset} changeset to be filtered
* @param filter {function} fnc which returns true if an
2012-02-26 19:59:11 +01:00
* attribute X (int) should be kept in the Changeset
*/
2011-03-26 14:10:41 +01:00
exports.filterAttribNumbers = function (cs, filter) {
return exports.mapAttribNumbers(cs, filter);
};
2012-02-26 19:59:11 +01:00
/**
* does exactly the same as exports.filterAttribNumbers
*/
2011-03-26 14:10:41 +01:00
exports.mapAttribNumbers = function (cs, func) {
2020-11-23 19:24:19 +01:00
let dollarPos = cs.indexOf('$');
2011-03-26 14:10:41 +01:00
if (dollarPos < 0) {
dollarPos = cs.length;
}
2020-11-23 19:24:19 +01:00
const upToDollar = cs.substring(0, dollarPos);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, (s, a) => {
const n = func(exports.parseNum(a));
2011-03-26 14:10:41 +01:00
if (n === true) {
return s;
2020-11-23 19:24:19 +01:00
} else if ((typeof n) === 'number') {
return `*${exports.numToString(n)}`;
2011-03-26 14:10:41 +01:00
} else {
return '';
}
});
return newUpToDollar + cs.substring(dollarPos);
};
2012-02-26 13:18:17 +01:00
/**
* Create a Changeset going from Identity to a certain state
* @params text {string} text of the final change
* @attribs attribs {string} optional, operations which insert
2012-02-26 13:18:17 +01:00
* the text and also puts the right attributes
*/
2011-03-26 14:10:41 +01:00
exports.makeAText = function (text, attribs) {
return {
2020-11-23 19:24:19 +01:00
text,
attribs: (attribs || exports.makeAttribution(text)),
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 19:59:11 +01:00
/**
* Apply a Changeset to a AText
2012-02-26 19:59:11 +01:00
* @param cs {Changeset} Changeset to be applied
* @param atext {AText}
2012-02-26 19:59:11 +01:00
* @param pool {AttribPool} Attribute Pool to add to
*/
2011-03-26 14:10:41 +01:00
exports.applyToAText = function (cs, atext, pool) {
return {
text: exports.applyToText(cs, atext.text),
2020-11-23 19:24:19 +01:00
attribs: exports.applyToAttribution(cs, atext.attribs, pool),
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 19:59:11 +01:00
/**
* Clones a AText structure
* @param atext {AText}
2012-02-26 19:59:11 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.cloneAText = function (atext) {
if (atext) {
return {
text: atext.text,
2020-11-23 19:24:19 +01:00
attribs: atext.attribs,
};
} else { exports.error('atext is null'); }
2011-03-26 14:10:41 +01:00
};
2012-02-26 19:59:11 +01:00
/**
* Copies a AText structure from atext1 to atext2
* @param atext {AText}
2012-02-26 19:59:11 +01:00
*/
2011-03-26 14:10:41 +01:00
exports.copyAText = function (atext1, atext2) {
atext2.text = atext1.text;
atext2.attribs = atext1.attribs;
};
2012-02-26 19:59:11 +01:00
/**
* Append the set of operations from atext to an assembler
* @param atext {AText}
2012-02-26 19:59:11 +01:00
* @param assem Assembler like smartOpAssembler
*/
2011-03-26 14:10:41 +01:00
exports.appendATextToAssembler = function (atext, assem) {
// intentionally skips last newline char of atext
2020-11-23 19:24:19 +01:00
const iter = exports.opIterator(atext.attribs);
const op = exports.newOp();
2011-03-26 14:10:41 +01:00
while (iter.hasNext()) {
iter.next(op);
if (!iter.hasNext()) {
// last op, exclude final newline
if (op.lines <= 1) {
op.lines = 0;
op.chars--;
if (op.chars) {
assem.append(op);
}
} else {
2020-11-23 19:24:19 +01:00
const nextToLastNewlineEnd =
2011-03-26 14:10:41 +01:00
atext.text.lastIndexOf('\n', atext.text.length - 2) + 1;
2020-11-23 19:24:19 +01:00
const lastLineLength = atext.text.length - nextToLastNewlineEnd - 1;
2011-03-26 14:10:41 +01:00
op.lines--;
op.chars -= (lastLineLength + 1);
assem.append(op);
op.lines = 0;
op.chars = lastLineLength;
if (op.chars) {
assem.append(op);
}
}
} else {
assem.append(op);
}
}
};
2012-02-26 19:59:11 +01:00
/**
* Creates a clone of a Changeset and it's APool
* @param cs {Changeset}
2012-02-26 19:59:11 +01:00
* @param pool {AtributePool}
*/
2011-03-26 14:10:41 +01:00
exports.prepareForWire = function (cs, pool) {
2020-11-23 19:24:19 +01:00
const newPool = new AttributePool();
const newCs = exports.moveOpsToNewPool(cs, pool, newPool);
2011-03-26 14:10:41 +01:00
return {
translated: newCs,
2020-11-23 19:24:19 +01:00
pool: newPool,
2011-03-26 14:10:41 +01:00
};
};
2012-02-26 19:59:11 +01:00
/**
* Checks if a changeset s the identity changeset
*/
2011-03-26 14:10:41 +01:00
exports.isIdentity = function (cs) {
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
return unpacked.ops == '' && unpacked.oldLen == unpacked.newLen;
2011-03-26 14:10:41 +01:00
};
2012-02-26 19:59:11 +01:00
/**
* returns all the values of attributes with a certain key
* in an Op attribs string
2012-02-26 19:59:11 +01:00
* @param attribs {string} Attribute string of a Op
* @param key {string} string to be seached for
* @param pool {AttribPool} attribute pool
*/
2011-03-26 14:10:41 +01:00
exports.opAttributeValue = function (op, key, pool) {
return exports.attribsAttributeValue(op.attribs, key, pool);
};
2012-02-26 19:59:11 +01:00
/**
* returns all the values of attributes with a certain key
* in an attribs string
2012-02-26 19:59:11 +01:00
* @param attribs {string} Attribute string
* @param key {string} string to be seached for
* @param pool {AttribPool} attribute pool
*/
2011-03-26 14:10:41 +01:00
exports.attribsAttributeValue = function (attribs, key, pool) {
2020-11-23 19:24:19 +01:00
let value = '';
2011-03-26 14:10:41 +01:00
if (attribs) {
2020-11-23 19:24:19 +01:00
exports.eachAttribNumber(attribs, (n) => {
2011-03-26 14:10:41 +01:00
if (pool.getAttribKey(n) == key) {
value = pool.getAttribValue(n);
}
});
}
return value;
};
2012-02-26 19:59:11 +01:00
/**
* Creates a Changeset builder for a string with initial
2012-02-26 19:59:11 +01:00
* length oldLen. Allows to add/remove parts of it
* @param oldLen {int} Old length
*/
2011-03-26 14:10:41 +01:00
exports.builder = function (oldLen) {
2020-11-23 19:24:19 +01:00
const assem = exports.smartOpAssembler();
const o = exports.newOp();
const charBank = exports.stringAssembler();
2011-03-26 14:10:41 +01:00
var self = {
// attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case)
2020-11-23 19:24:19 +01:00
keep(N, L, attribs, pool) {
2011-03-26 14:10:41 +01:00
o.opcode = '=';
o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || '';
o.chars = N;
o.lines = (L || 0);
assem.append(o);
return self;
},
2020-11-23 19:24:19 +01:00
keepText(text, attribs, pool) {
2011-03-26 14:10:41 +01:00
assem.appendOpWithText('=', text, attribs, pool);
return self;
},
2020-11-23 19:24:19 +01:00
insert(text, attribs, pool) {
2011-03-26 14:10:41 +01:00
assem.appendOpWithText('+', text, attribs, pool);
charBank.append(text);
return self;
},
2020-11-23 19:24:19 +01:00
remove(N, L) {
2011-03-26 14:10:41 +01:00
o.opcode = '-';
o.attribs = '';
o.chars = N;
o.lines = (L || 0);
assem.append(o);
return self;
},
2020-11-23 19:24:19 +01:00
toString() {
2011-03-26 14:10:41 +01:00
assem.endDocument();
2020-11-23 19:24:19 +01:00
const newLen = oldLen + assem.getLengthChange();
2011-03-26 14:10:41 +01:00
return exports.pack(oldLen, newLen, assem.toString(), charBank.toString());
2020-11-23 19:24:19 +01:00
},
2011-03-26 14:10:41 +01:00
};
return self;
};
exports.makeAttribsString = function (opcode, attribs, pool) {
// makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work
if (!attribs) {
return '';
2020-11-23 19:24:19 +01:00
} else if ((typeof attribs) === 'string') {
2011-03-26 14:10:41 +01:00
return attribs;
} else if (pool && attribs && attribs.length) {
if (attribs.length > 1) {
attribs = attribs.slice();
attribs.sort();
}
2020-11-23 19:24:19 +01:00
const result = [];
for (let i = 0; i < attribs.length; i++) {
const pair = attribs[i];
2011-03-26 14:10:41 +01:00
if (opcode == '=' || (opcode == '+' && pair[1])) {
2020-11-23 19:24:19 +01:00
result.push(`*${exports.numToString(pool.putAttrib(pair))}`);
2011-03-26 14:10:41 +01:00
}
}
return result.join('');
}
};
// like "substring" but on a single-line attribution string
exports.subattribution = function (astr, start, optEnd) {
2020-11-23 19:24:19 +01:00
const iter = exports.opIterator(astr, 0);
const assem = exports.smartOpAssembler();
const attOp = exports.newOp();
const csOp = exports.newOp();
const opOut = exports.newOp();
2011-03-26 14:10:41 +01:00
function doCsOp() {
if (csOp.chars) {
while (csOp.opcode && (attOp.opcode || iter.hasNext())) {
if (!attOp.opcode) iter.next(attOp);
if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) {
csOp.lines++;
}
exports._slicerZipperFunc(attOp, csOp, opOut, null);
if (opOut.opcode) {
assem.append(opOut);
opOut.opcode = '';
}
}
}
}
csOp.opcode = '-';
csOp.chars = start;
doCsOp();
if (optEnd === undefined) {
if (attOp.opcode) {
assem.append(attOp);
}
while (iter.hasNext()) {
iter.next(attOp);
assem.append(attOp);
}
} else {
csOp.opcode = '=';
csOp.chars = optEnd - start;
doCsOp();
}
return assem.toString();
};
exports.inverse = function (cs, lines, alines, pool) {
// lines and alines are what the exports is meant to apply to.
// They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines.
function lines_get(idx) {
if (lines.get) {
return lines.get(idx);
} else {
return lines[idx];
}
}
function alines_get(idx) {
if (alines.get) {
return alines.get(idx);
} else {
return alines[idx];
}
}
2020-11-23 19:24:19 +01:00
let curLine = 0;
let curChar = 0;
let curLineOpIter = null;
let curLineOpIterLine;
const curLineNextOp = exports.newOp('+');
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const unpacked = exports.unpack(cs);
const csIter = exports.opIterator(unpacked.ops);
const builder = exports.builder(unpacked.newLen);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
function consumeAttribRuns(numChars, func /* (len, attribs, endsLine)*/) {
2011-03-26 14:10:41 +01:00
if ((!curLineOpIter) || (curLineOpIterLine != curLine)) {
// create curLineOpIter and advance it to curChar
curLineOpIter = exports.opIterator(alines_get(curLine));
curLineOpIterLine = curLine;
2020-11-23 19:24:19 +01:00
let indexIntoLine = 0;
let done = false;
2014-02-20 11:08:49 +01:00
while (!done && curLineOpIter.hasNext()) {
2011-03-26 14:10:41 +01:00
curLineOpIter.next(curLineNextOp);
if (indexIntoLine + curLineNextOp.chars >= curChar) {
curLineNextOp.chars -= (curChar - indexIntoLine);
done = true;
} else {
indexIntoLine += curLineNextOp.chars;
}
}
}
while (numChars > 0) {
if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
curLine++;
curChar = 0;
curLineOpIterLine = curLine;
curLineNextOp.chars = 0;
curLineOpIter = exports.opIterator(alines_get(curLine));
}
if (!curLineNextOp.chars) {
curLineOpIter.next(curLineNextOp);
}
2020-11-23 19:24:19 +01:00
const charsToUse = Math.min(numChars, curLineNextOp.chars);
2011-03-26 14:10:41 +01:00
func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0);
numChars -= charsToUse;
curLineNextOp.chars -= charsToUse;
curChar += charsToUse;
}
if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
curLine++;
curChar = 0;
}
}
function skip(N, L) {
if (L) {
curLine += L;
curChar = 0;
2020-11-23 19:24:19 +01:00
} else if (curLineOpIter && curLineOpIterLine == curLine) {
consumeAttribRuns(N, () => {});
2011-03-26 14:10:41 +01:00
} else {
2020-11-23 19:24:19 +01:00
curChar += N;
2011-03-26 14:10:41 +01:00
}
}
function nextText(numChars) {
2020-11-23 19:24:19 +01:00
let len = 0;
const assem = exports.stringAssembler();
const firstString = lines_get(curLine).substring(curChar);
2011-03-26 14:10:41 +01:00
len += firstString.length;
assem.append(firstString);
2020-11-23 19:24:19 +01:00
let lineNum = curLine + 1;
2011-03-26 14:10:41 +01:00
while (len < numChars) {
2020-11-23 19:24:19 +01:00
const nextString = lines_get(lineNum);
2011-03-26 14:10:41 +01:00
len += nextString.length;
assem.append(nextString);
lineNum++;
}
return assem.toString().substring(0, numChars);
}
function cachedStrFunc(func) {
2020-11-23 19:24:19 +01:00
const cache = {};
2011-03-26 14:10:41 +01:00
return function (s) {
if (!cache[s]) {
cache[s] = func(s);
}
return cache[s];
};
}
2020-11-23 19:24:19 +01:00
const attribKeys = [];
const attribValues = [];
2011-03-26 14:10:41 +01:00
while (csIter.hasNext()) {
2020-11-23 19:24:19 +01:00
const csOp = csIter.next();
2011-03-26 14:10:41 +01:00
if (csOp.opcode == '=') {
if (csOp.attribs) {
attribKeys.length = 0;
attribValues.length = 0;
2020-11-23 19:24:19 +01:00
exports.eachAttribNumber(csOp.attribs, (n) => {
2011-03-26 14:10:41 +01:00
attribKeys.push(pool.getAttribKey(n));
attribValues.push(pool.getAttribValue(n));
});
2020-11-23 19:24:19 +01:00
var undoBackToAttribs = cachedStrFunc((attribs) => {
const backAttribs = [];
for (let i = 0; i < attribKeys.length; i++) {
const appliedKey = attribKeys[i];
const appliedValue = attribValues[i];
const oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool);
2011-03-26 14:10:41 +01:00
if (appliedValue != oldValue) {
backAttribs.push([appliedKey, oldValue]);
}
}
return exports.makeAttribsString('=', backAttribs, pool);
});
2020-11-23 19:24:19 +01:00
consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => {
2011-03-26 14:10:41 +01:00
builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs));
});
} else {
skip(csOp.chars, csOp.lines);
builder.keep(csOp.chars, csOp.lines);
}
} else if (csOp.opcode == '+') {
builder.remove(csOp.chars, csOp.lines);
} else if (csOp.opcode == '-') {
var textBank = nextText(csOp.chars);
var textBankIndex = 0;
2020-11-23 19:24:19 +01:00
consumeAttribRuns(csOp.chars, (len, attribs, endsLine) => {
2011-03-26 14:10:41 +01:00
builder.insert(textBank.substr(textBankIndex, len), attribs);
textBankIndex += len;
});
}
}
return exports.checkRep(builder.toString());
};
// %CLIENT FILE ENDS HERE%
exports.follow = function (cs1, cs2, reverseInsertOrder, pool) {
2020-11-23 19:24:19 +01:00
const unpacked1 = exports.unpack(cs1);
const unpacked2 = exports.unpack(cs2);
const len1 = unpacked1.oldLen;
const len2 = unpacked2.oldLen;
exports.assert(len1 == len2, 'mismatched follow - cannot transform cs1 on top of cs2');
const chars1 = exports.stringIterator(unpacked1.charBank);
const chars2 = exports.stringIterator(unpacked2.charBank);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const oldLen = unpacked1.newLen;
let oldPos = 0;
let newLen = 0;
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool);
2011-03-26 14:10:41 +01:00
2020-11-23 19:24:19 +01:00
const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => {
2011-03-26 14:10:41 +01:00
if (op1.opcode == '+' || op2.opcode == '+') {
2020-11-23 19:24:19 +01:00
let whichToDo;
2011-03-26 14:10:41 +01:00
if (op2.opcode != '+') {
whichToDo = 1;
} else if (op1.opcode != '+') {
whichToDo = 2;
} else {
// both +
2020-11-23 19:24:19 +01:00
const firstChar1 = chars1.peek(1);
const firstChar2 = chars2.peek(1);
const insertFirst1 = hasInsertFirst(op1.attribs);
const insertFirst2 = hasInsertFirst(op2.attribs);
2011-03-26 14:10:41 +01:00
if (insertFirst1 && !insertFirst2) {
whichToDo = 1;
} else if (insertFirst2 && !insertFirst1) {
whichToDo = 2;
}
// insert string that doesn't start with a newline first so as not to break up lines
else if (firstChar1 == '\n' && firstChar2 != '\n') {
whichToDo = 2;
} else if (firstChar1 != '\n' && firstChar2 == '\n') {
whichToDo = 1;
}
// break symmetry:
else if (reverseInsertOrder) {
whichToDo = 2;
} else {
whichToDo = 1;
}
}
if (whichToDo == 1) {
chars1.skip(op1.chars);
opOut.opcode = '=';
opOut.lines = op1.lines;
opOut.chars = op1.chars;
opOut.attribs = '';
op1.opcode = '';
} else {
// whichToDo == 2
chars2.skip(op2.chars);
exports.copyOp(op2, opOut);
op2.opcode = '';
}
} else if (op1.opcode == '-') {
if (!op2.opcode) {
op1.opcode = '';
2020-11-23 19:24:19 +01:00
} else if (op1.chars <= op2.chars) {
op2.chars -= op1.chars;
op2.lines -= op1.lines;
op1.opcode = '';
if (!op2.chars) {
2011-03-26 14:10:41 +01:00
op2.opcode = '';
}
2020-11-23 19:24:19 +01:00
} else {
op1.chars -= op2.chars;
op1.lines -= op2.lines;
op2.opcode = '';
2011-03-26 14:10:41 +01:00
}
} else if (op2.opcode == '-') {
exports.copyOp(op2, opOut);
if (!op1.opcode) {
op2.opcode = '';
} else if (op2.chars <= op1.chars) {
// delete part or all of a keep
op1.chars -= op2.chars;
op1.lines -= op2.lines;
op2.opcode = '';
if (!op1.chars) {
op1.opcode = '';
}
} else {
// delete all of a keep, and keep going
opOut.lines = op1.lines;
opOut.chars = op1.chars;
op2.lines -= op1.lines;
op2.chars -= op1.chars;
op1.opcode = '';
}
} else if (!op1.opcode) {
exports.copyOp(op2, opOut);
op2.opcode = '';
} else if (!op2.opcode) {
Issue #1625: Fix to client-side-induced changeset spamming. THE BUG - HIGH LEVEL: - When client A sends out an attribute change, client B applies that change to itself but also thinks that it made the change itself, which is incorrect. This means that when client B next makes a change, he will send out that he made the attrib change that A actually made. - Ex: Have 2 clients on the same pad. Have A apply bold on some text. Next, have B type a character. B will broadcast that it both added a character AND applied bold, when in reality it did NOT apply bold at all, that change was done by the other client and this client incorrectly adopted it as its own. - This root bug behavior results in clients continuing to think that they each made the other client's change, thus resulting in an infinite loop of changeset spamming that bogs down clients and harms server stability. THE BUG - IN DEPTH: - The root issue is in the way that Changesets are combined in Changeset.follow(). Specifically, in the case of a changeset with ONLY new attrib changes (no text changes) being merged with an identity changeset (has no ops). - In this case, Changeset.follow() copies the ops of the new CS and fully overrides the other CS. - applyChangesToBase invokes Changeset.follow to determine the final client document CS state after applying the new CS. If the final client document CS state is NOT the identity CS, then the client broadcasts that it made a change. - When client A changes just attribs, client B's applyChangesToBase calls Changeset.follow() and passes client A's changeset (attrib change) and Client B's current changeset state (identity). - As per the noted bug, Changeset.follow() returns client A's changeset as a result, causing client B to adopt client A's changeset as its own document state. Thus, client A ends up thinking it has made client B's changes. THE FIX: - Changeset.follow() should NOT copy the ops of the new CS passed in if those changes are only attrib changes. This allows applyChangesToBase to properly set the client's CS back to the identity after applying an external attrib change, instead of incorrectly adopting the external client's changes.
2013-04-25 00:02:58 +02:00
// @NOTE: Critical bugfix for EPL issue #1625. We do not copy op1 here
// in order to prevent attributes from leaking into result changesets.
// exports.copyOp(op1, opOut);
2011-03-26 14:10:41 +01:00
op1.opcode = '';
} else {
// both keeps
opOut.opcode = '=';
opOut.attribs = exports.followAttributes(op1.attribs, op2.attribs, pool);
if (op1.chars <= op2.chars) {
opOut.chars = op1.chars;
opOut.lines = op1.lines;
op2.chars -= op1.chars;
op2.lines -= op1.lines;
op1.opcode = '';
if (!op2.chars) {
op2.opcode = '';
}
} else {
opOut.chars = op2.chars;
opOut.lines = op2.lines;
op1.chars -= op2.chars;
op1.lines -= op2.lines;
op2.opcode = '';
}
}
switch (opOut.opcode) {
2020-11-23 19:24:19 +01:00
case '=':
oldPos += opOut.chars;
newLen += opOut.chars;
break;
case '-':
oldPos += opOut.chars;
break;
case '+':
newLen += opOut.chars;
break;
2011-03-26 14:10:41 +01:00
}
});
newLen += oldLen - oldPos;
return exports.pack(oldLen, newLen, newOps, unpacked2.charBank);
};
exports.followAttributes = function (att1, att2, pool) {
// The merge of two sets of attribute changes to the same text
// takes the lexically-earlier value if there are two values
// for the same key. Otherwise, all key/value changes from
// both attribute sets are taken. This operation is the "follow",
// so a set of changes is produced that can be applied to att1
// to produce the merged set.
if ((!att2) || (!pool)) return '';
if (!att1) return att2;
2020-11-23 19:24:19 +01:00
const atts = [];
att2.replace(/\*([0-9a-z]+)/g, (_, a) => {
2011-03-26 14:10:41 +01:00
atts.push(pool.getAttrib(exports.parseNum(a)));
return '';
});
2020-11-23 19:24:19 +01:00
att1.replace(/\*([0-9a-z]+)/g, (_, a) => {
const pair1 = pool.getAttrib(exports.parseNum(a));
for (let i = 0; i < atts.length; i++) {
const pair2 = atts[i];
2011-03-26 14:10:41 +01:00
if (pair1[0] == pair2[0]) {
if (pair1[1] <= pair2[1]) {
// winner of merge is pair1, delete this attribute
atts.splice(i, 1);
}
break;
}
}
return '';
});
// we've only removed attributes, so they're already sorted
2020-11-23 19:24:19 +01:00
const buf = exports.stringAssembler();
for (let i = 0; i < atts.length; i++) {
2011-03-26 14:10:41 +01:00
buf.append('*');
buf.append(exports.numToString(pool.putAttrib(atts[i])));
}
return buf.toString();
};
exports.composeWithDeletions = function (cs1, cs2, pool) {
2020-11-23 19:24:19 +01:00
const unpacked1 = exports.unpack(cs1);
const unpacked2 = exports.unpack(cs2);
const len1 = unpacked1.oldLen;
const len2 = unpacked1.newLen;
exports.assert(len2 == unpacked2.oldLen, 'mismatched composition of two changesets');
const len3 = unpacked2.newLen;
const bankIter1 = exports.stringIterator(unpacked1.charBank);
const bankIter2 = exports.stringIterator(unpacked2.charBank);
const bankAssem = exports.stringAssembler();
const newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, (op1, op2, opOut) => {
const op1code = op1.opcode;
const op2code = op2.opcode;
if (op1code == '+' && op2code == '-') {
bankIter1.skip(Math.min(op1.chars, op2.chars));
}
exports._slicerZipperFuncWithDeletions(op1, op2, opOut, pool);
if (opOut.opcode == '+') {
if (op2code == '+') {
bankAssem.append(bankIter2.take(opOut.chars));
} else {
bankAssem.append(bankIter1.take(opOut.chars));
}
}
});
return exports.pack(len1, len3, newOps, bankAssem.toString());
};
// This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly.
// This is necassary for correct paddiff. But to ensure these changes doesn't affect anything else, we've created a seperate function only used for paddiffs
2020-11-23 19:24:19 +01:00
exports._slicerZipperFuncWithDeletions = function (attOp, csOp, opOut, pool) {
// attOp is the op from the sequence that is being operated on, either an
// attribution string or the earlier of two exportss being composed.
// pool can be null if definitely not needed.
2020-11-23 19:24:19 +01:00
// print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
if (attOp.opcode == '-') {
exports.copyOp(attOp, opOut);
attOp.opcode = '';
} else if (!attOp.opcode) {
exports.copyOp(csOp, opOut);
csOp.opcode = '';
} else {
switch (csOp.opcode) {
2020-11-23 19:24:19 +01:00
case '-':
{
if (csOp.chars <= attOp.chars) {
// delete or delete part
if (attOp.opcode == '=') {
opOut.opcode = '-';
opOut.chars = csOp.chars;
opOut.lines = csOp.lines;
2020-11-23 19:24:19 +01:00
opOut.attribs = csOp.attribs; // changed by yammer
}
attOp.chars -= csOp.chars;
attOp.lines -= csOp.lines;
csOp.opcode = '';
if (!attOp.chars) {
attOp.opcode = '';
}
} else {
// delete and keep going
if (attOp.opcode == '=') {
opOut.opcode = '-';
opOut.chars = attOp.chars;
opOut.lines = attOp.lines;
2020-11-23 19:24:19 +01:00
opOut.attribs = csOp.attribs; // changed by yammer
}
csOp.chars -= attOp.chars;
csOp.lines -= attOp.lines;
attOp.opcode = '';
}
break;
}
2020-11-23 19:24:19 +01:00
case '+':
{
// insert
exports.copyOp(csOp, opOut);
csOp.opcode = '';
break;
}
2020-11-23 19:24:19 +01:00
case '=':
{
if (csOp.chars <= attOp.chars) {
// keep or keep part
opOut.opcode = attOp.opcode;
opOut.chars = csOp.chars;
opOut.lines = csOp.lines;
opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
csOp.opcode = '';
attOp.chars -= csOp.chars;
attOp.lines -= csOp.lines;
if (!attOp.chars) {
attOp.opcode = '';
}
} else {
// keep and keep going
opOut.opcode = attOp.opcode;
opOut.chars = attOp.chars;
opOut.lines = attOp.lines;
opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
attOp.opcode = '';
csOp.chars -= attOp.chars;
csOp.lines -= attOp.lines;
}
break;
}
2020-11-23 19:24:19 +01:00
case '':
{
exports.copyOp(attOp, opOut);
attOp.opcode = '';
break;
}
}
}
};