pad.pub0.org/static/js/easysync2.js

1969 lines
53 KiB
JavaScript

// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.easysync2
// %APPJET%: jimport("com.etherpad.Easysync2Support");
/**
* Copyright 2009 Google Inc.
*
* 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.
*/
//var _opt = (this.Easysync2Support || null);
var _opt = null; // disable optimization for now
function AttribPool() {
var p = {};
p.numToAttrib = {}; // e.g. {0: ['foo','bar']}
p.attribToNum = {}; // e.g. {'foo,bar': 0}
p.nextNum = 0;
p.putAttrib = function(attrib, dontAddIfAbsent) {
var str = String(attrib);
if (str in p.attribToNum) {
return p.attribToNum[str];
}
if (dontAddIfAbsent) {
return -1;
}
var num = p.nextNum++;
p.attribToNum[str] = num;
p.numToAttrib[num] = [String(attrib[0]||''),
String(attrib[1]||'')];
return num;
};
p.getAttrib = function(num) {
var pair = p.numToAttrib[num];
if (! pair) return pair;
return [pair[0], pair[1]]; // return a mutable copy
};
p.getAttribKey = function(num) {
var pair = p.numToAttrib[num];
if (! pair) return '';
return pair[0];
};
p.getAttribValue = function(num) {
var pair = p.numToAttrib[num];
if (! pair) return '';
return pair[1];
};
p.eachAttrib = function(func) {
for(var n in p.numToAttrib) {
var pair = p.numToAttrib[n];
func(pair[0], pair[1]);
}
};
p.toJsonable = function() {
return {numToAttrib: p.numToAttrib, nextNum: p.nextNum};
};
p.fromJsonable = function(obj) {
p.numToAttrib = obj.numToAttrib;
p.nextNum = obj.nextNum;
p.attribToNum = {};
for(var n in p.numToAttrib) {
p.attribToNum[String(p.numToAttrib[n])] = Number(n);
}
return p;
};
return p;
}
var Changeset = {};
Changeset.error = function error(msg) { var e = new Error(msg); e.easysync = true; throw e; };
Changeset.assert = function assert(b, msgParts) {
if (! b) {
var msg = Array.prototype.slice.call(arguments, 1).join('');
Changeset.error("Changeset: "+msg);
}
};
Changeset.parseNum = function(str) { return parseInt(str, 36); };
Changeset.numToString = function(num) { return num.toString(36).toLowerCase(); };
Changeset.toBaseTen = function(cs) {
var dollarIndex = cs.indexOf('$');
var beforeDollar = cs.substring(0, dollarIndex);
var fromDollar = cs.substring(dollarIndex);
return beforeDollar.replace(/[0-9a-z]+/g, function(s) {
return String(Changeset.parseNum(s)); }) + fromDollar;
};
Changeset.oldLen = function(cs) {
return Changeset.unpack(cs).oldLen;
};
Changeset.newLen = function(cs) {
return Changeset.unpack(cs).newLen;
};
Changeset.opIterator = function(opsStr, optStartIndex) {
//print(opsStr);
var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g;
var startIndex = (optStartIndex || 0);
var curIndex = startIndex;
var prevIndex = curIndex;
function nextRegexMatch() {
prevIndex = curIndex;
var result;
if (_opt) {
result = _opt.nextOpInString(opsStr, curIndex);
if (result) {
if (result.opcode() == '?') {
Changeset.error("Hit error opcode in op stream");
}
curIndex = result.lastIndex();
}
}
else {
regex.lastIndex = curIndex;
result = regex.exec(opsStr);
curIndex = regex.lastIndex;
if (result[0] == '?') {
Changeset.error("Hit error opcode in op stream");
}
}
return result;
}
var regexResult = nextRegexMatch();
var obj = Changeset.newOp();
function next(optObj) {
var op = (optObj || obj);
if (_opt && regexResult) {
op.attribs = regexResult.attribs();
op.lines = regexResult.lines();
op.chars = regexResult.chars();
op.opcode = regexResult.opcode();
regexResult = nextRegexMatch();
}
else if ((! _opt) && regexResult[0]) {
op.attribs = regexResult[1];
op.lines = Changeset.parseNum(regexResult[2] || 0);
op.opcode = regexResult[3];
op.chars = Changeset.parseNum(regexResult[4]);
regexResult = nextRegexMatch();
}
else {
Changeset.clearOp(op);
}
return op;
}
function hasNext() { return !! (_opt ? regexResult : regexResult[0]); }
function lastIndex() { return prevIndex; }
return {next: next, hasNext: hasNext, lastIndex: lastIndex};
};
Changeset.clearOp = function(op) {
op.opcode = '';
op.chars = 0;
op.lines = 0;
op.attribs = '';
};
Changeset.newOp = function(optOpcode) {
return {opcode:(optOpcode || ''), chars:0, lines:0, attribs:''};
};
Changeset.cloneOp = function(op) {
return {opcode: op.opcode, chars: op.chars, lines: op.lines, attribs: op.attribs};
};
Changeset.copyOp = function(op1, op2) {
op2.opcode = op1.opcode;
op2.chars = op1.chars;
op2.lines = op1.lines;
op2.attribs = op1.attribs;
};
Changeset.opString = function(op) {
// just for debugging
if (! op.opcode) return 'null';
var assem = Changeset.opAssembler();
assem.append(op);
return assem.toString();
};
Changeset.stringOp = function(str) {
// just for debugging
return Changeset.opIterator(str).next();
};
Changeset.checkRep = function(cs) {
// doesn't check things that require access to attrib pool (e.g. attribute order)
// or original string (e.g. newline positions)
var unpacked = Changeset.unpack(cs);
var oldLen = unpacked.oldLen;
var newLen = unpacked.newLen;
var ops = unpacked.ops;
var charBank = unpacked.charBank;
var assem = Changeset.smartOpAssembler();
var oldPos = 0;
var calcNewLen = 0;
var numInserted = 0;
var iter = Changeset.opIterator(ops);
while (iter.hasNext()) {
var o = iter.next();
switch (o.opcode) {
case '=': oldPos += o.chars; calcNewLen += o.chars; break;
case '-': oldPos += o.chars; Changeset.assert(oldPos < oldLen, oldPos," >= ",oldLen," in ",cs); break;
case '+': {
calcNewLen += o.chars; numInserted += o.chars;
Changeset.assert(calcNewLen < newLen, calcNewLen," >= ",newLen," in ",cs);
break;
}
}
assem.append(o);
}
calcNewLen += oldLen - oldPos;
charBank = charBank.substring(0, numInserted);
while (charBank.length < numInserted) {
charBank += "?";
}
assem.endDocument();
var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank);
Changeset.assert(normalized == cs, normalized,' != ',cs);
return cs;
}
Changeset.smartOpAssembler = function() {
// Like opAssembler but able to produce conforming changesets
// 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)
var minusAssem = Changeset.mergingOpAssembler();
var plusAssem = Changeset.mergingOpAssembler();
var keepAssem = Changeset.mergingOpAssembler();
var assem = Changeset.stringAssembler();
var lastOpcode = '';
var lengthChange = 0;
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) {
var op = Changeset.newOp(opcode);
op.attribs = Changeset.makeAttribsString(opcode, attribs, pool);
var lastNewlinePos = text.lastIndexOf('\n');
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 {append: append, toString: toString, clear: clear, endDocument: endDocument,
appendOpWithText: appendOpWithText, getLengthChange: getLengthChange };
};
if (_opt) {
Changeset.mergingOpAssembler = function() {
var assem = _opt.mergingOpAssembler();
function append(op) {
assem.append(op.opcode, op.chars, op.lines, op.attribs);
}
function toString() {
return assem.toString();
}
function clear() {
assem.clear();
}
function endDocument() {
assem.endDocument();
}
return {append: append, toString: toString, clear: clear, endDocument: endDocument};
};
}
else {
Changeset.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.
var assem = Changeset.opAssembler();
var bufOp = Changeset.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.
var 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;
assem.append(bufOp);
bufOpAdditionalCharsAfterNewline = 0;
}
}
bufOp.opcode = '';
}
}
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;
}
else {
// append in-line text to multi-line bufOp
bufOpAdditionalCharsAfterNewline += op.chars;
}
}
else {
flush();
Changeset.copyOp(op, bufOp);
}
}
}
function endDocument() {
flush(true);
}
function toString() {
flush();
return assem.toString();
}
function clear() {
assem.clear();
Changeset.clearOp(bufOp);
}
return {append: append, toString: toString, clear: clear, endDocument: endDocument};
};
}
if (_opt) {
Changeset.opAssembler = function() {
var assem = _opt.opAssembler();
// this function allows op to be mutated later (doesn't keep a ref)
function append(op) {
assem.append(op.opcode, op.chars, op.lines, op.attribs);
}
function toString() {
return assem.toString();
}
function clear() {
assem.clear();
}
return {append: append, toString: toString, clear: clear};
};
}
else {
Changeset.opAssembler = function() {
var pieces = [];
// this function allows op to be mutated later (doesn't keep a ref)
function append(op) {
pieces.push(op.attribs);
if (op.lines) {
pieces.push('|', Changeset.numToString(op.lines));
}
pieces.push(op.opcode);
pieces.push(Changeset.numToString(op.chars));
}
function toString() {
return pieces.join('');
}
function clear() {
pieces.length = 0;
}
return {append: append, toString: toString, clear: clear};
};
}
Changeset.stringIterator = function(str) {
var curIndex = 0;
function assertRemaining(n) {
Changeset.assert(n <= remaining(), "!(",n," <= ",remaining(),")");
}
function take(n) {
assertRemaining(n);
var s = str.substr(curIndex, n);
curIndex += n;
return s;
}
function peek(n) {
assertRemaining(n);
var s = str.substr(curIndex, n);
return s;
}
function skip(n) {
assertRemaining(n);
curIndex += n;
}
function remaining() {
return str.length - curIndex;
}
return {take:take, skip:skip, remaining:remaining, peek:peek};
};
Changeset.stringAssembler = function() {
var pieces = [];
function append(x) {
pieces.push(String(x));
}
function toString() {
return pieces.join('');
}
return {append: append, toString: toString};
};
// "lines" need not be an array as long as it supports certain calls (lines_foo inside).
Changeset.textLinesMutator = function(lines) {
// Mutates lines, an array of strings, in place.
// Mutation operations have the same constraints as changeset 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.
var curSplice = [0,0];
var inSplice = false;
// position in document after curSplice is applied:
var curLine = 0, curCol = 0;
// 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() {
if ((typeof lines.length) == "number") {
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) {
print(typ+": "+curSplice.toSource()+" / "+curLine+","+curCol+" / "+lines_toSource());
}
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();
}
for(var i=0;i<L;i++) {
curCol = 0;
putCurLineInSplice();
curLine++;
}
}
else {
if (inSplice) {
if (L > 1) {
leaveSplice();
}
else {
putCurLineInSplice();
}
}
curLine += L;
curCol = 0;
}
//print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length);
/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) {
print("BLAH");
putCurLineInSplice();
}*/ // tests case foo in remove(), which isn't otherwise covered in current impl
}
//debugPrint("skip");
}
function skip(N, L, includeInSplice) {
if (N) {
if (L) {
skipLines(L, includeInSplice);
}
else {
if (includeInSplice && ! inSplice) {
enterSplice();
}
if (inSplice) {
putCurLineInSplice();
}
curCol += N;
//debugPrint("skip");
}
}
}
function removeLines(L) {
var removed = '';
if (L) {
if (! inSplice) {
enterSplice();
}
function nextKLinesText(k) {
var m = curSplice[0] + curSplice[1];
return lines_slice(m, m+k).join('');
}
if (isCurLineInSplice()) {
//print(curCol);
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;
var sline = curSplice.length - 1;
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;
}
//debugPrint("remove");
}
return removed;
}
function remove(N, L) {
var removed = '';
if (N) {
if (L) {
return removeLines(L);
}
else {
if (! inSplice) {
enterSplice();
}
var sline = putCurLineInSplice();
removed = curSplice[sline].substring(curCol, curCol+N);
curSplice[sline] = curSplice[sline].substring(0, curCol) +
curSplice[sline].substring(curCol+N);
//debugPrint("remove");
}
}
return removed;
}
function insert(text, L) {
if (text) {
if (! inSplice) {
enterSplice();
}
if (L) {
var newLines = Changeset.splitTextLines(text);
if (isCurLineInSplice()) {
//if (curCol == 0) {
//curSplice.length--;
//curSplice[1]--;
//Array.prototype.push.apply(curSplice, newLines);
//curLine += newLines.length;
//}
//else {
var sline = curSplice.length - 1;
var theLine = curSplice[sline];
var lineCol = curCol;
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;
//}
}
else {
Array.prototype.push.apply(curSplice, newLines);
curLine += newLines.length;
}
}
else {
var sline = putCurLineInSplice();
curSplice[sline] = curSplice[sline].substring(0, curCol) +
text + curSplice[sline].substring(curCol);
curCol += text.length;
}
//debugPrint("insert");
}
}
function hasMore() {
//print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]);
var docLines = lines_length();
if (inSplice) {
docLines += curSplice.length - 2 - curSplice[1];
}
return curLine < docLines;
}
function close() {
if (inSplice) {
leaveSplice();
}
//debugPrint("close");
}
var self = {skip:skip, remove:remove, insert:insert, close:close, hasMore:hasMore,
removeLines:removeLines, skipLines: skipLines};
return self;
};
Changeset.applyZip = function(in1, idx1, in2, idx2, func) {
var iter1 = Changeset.opIterator(in1, idx1);
var iter2 = Changeset.opIterator(in2, idx2);
var assem = Changeset.smartOpAssembler();
var op1 = Changeset.newOp();
var op2 = Changeset.newOp();
var opOut = Changeset.newOp();
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) {
//print(opOut.toSource());
assem.append(opOut);
opOut.opcode = '';
}
}
assem.endDocument();
return assem.toString();
};
Changeset.unpack = function(cs) {
var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
var headerMatch = headerRegex.exec(cs);
if ((! headerMatch) || (! headerMatch[0])) {
Changeset.error("Not a changeset: "+cs);
}
var oldLen = Changeset.parseNum(headerMatch[1]);
var changeSign = (headerMatch[2] == '>') ? 1 : -1;
var changeMag = Changeset.parseNum(headerMatch[3]);
var newLen = oldLen + changeSign*changeMag;
var opsStart = headerMatch[0].length;
var opsEnd = cs.indexOf("$");
if (opsEnd < 0) opsEnd = cs.length;
return {oldLen: oldLen, newLen: newLen, ops: cs.substring(opsStart, opsEnd),
charBank: cs.substring(opsEnd+1)};
};
Changeset.pack = function(oldLen, newLen, opsStr, bank) {
var lenDiff = newLen - oldLen;
var lenDiffStr = (lenDiff >= 0 ?
'>'+Changeset.numToString(lenDiff) :
'<'+Changeset.numToString(-lenDiff));
var a = [];
a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank);
return a.join('');
};
Changeset.applyToText = function(cs, str) {
var unpacked = Changeset.unpack(cs);
Changeset.assert(str.length == unpacked.oldLen,
"mismatched apply: ",str.length," / ",unpacked.oldLen);
var csIter = Changeset.opIterator(unpacked.ops);
var bankIter = Changeset.stringIterator(unpacked.charBank);
var strIter = Changeset.stringIterator(str);
var assem = Changeset.stringAssembler();
while (csIter.hasNext()) {
var op = csIter.next();
switch(op.opcode) {
case '+': assem.append(bankIter.take(op.chars)); break;
case '-': strIter.skip(op.chars); break;
case '=': assem.append(strIter.take(op.chars)); break;
}
}
assem.append(strIter.take(strIter.remaining()));
return assem.toString();
};
Changeset.mutateTextLines = function(cs, lines) {
var unpacked = Changeset.unpack(cs);
var csIter = Changeset.opIterator(unpacked.ops);
var bankIter = Changeset.stringIterator(unpacked.charBank);
var mut = Changeset.textLinesMutator(lines);
while (csIter.hasNext()) {
var op = csIter.next();
switch(op.opcode) {
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;
}
}
mut.close();
};
Changeset.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 changesets),
// 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;
var atts = [];
att1.replace(/\*([0-9a-z]+)/g, function(_, a) {
atts.push(pool.getAttrib(Changeset.parseNum(a)));
return '';
});
att2.replace(/\*([0-9a-z]+)/g, function(_, a) {
var pair = pool.getAttrib(Changeset.parseNum(a));
var found = false;
for(var i=0;i<atts.length;i++) {
var oldPair = atts[i];
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();
var buf = Changeset.stringAssembler();
for(var i=0;i<atts.length;i++) {
buf.append('*');
buf.append(Changeset.numToString(pool.putAttrib(atts[i])));
}
//print(att1+" / "+att2+" / "+buf.toString());
return buf.toString();
};
Changeset._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 changesets being composed.
// pool can be null if definitely not needed.
//print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
if (attOp.opcode == '-') {
Changeset.copyOp(attOp, opOut);
attOp.opcode = '';
}
else if (! attOp.opcode) {
Changeset.copyOp(csOp, opOut);
csOp.opcode = '';
}
else {
switch (csOp.opcode) {
case '-': {
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;
}
case '+': {
// insert
Changeset.copyOp(csOp, opOut);
csOp.opcode = '';
break;
}
case '=': {
if (csOp.chars <= attOp.chars) {
// keep or keep part
opOut.opcode = attOp.opcode;
opOut.chars = csOp.chars;
opOut.lines = csOp.lines;
opOut.attribs = Changeset.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 = Changeset.composeAttributes(attOp.attribs, csOp.attribs,
attOp.opcode == '=', pool);
attOp.opcode = '';
csOp.chars -= attOp.chars;
csOp.lines -= attOp.lines;
}
break;
}
case '': {
Changeset.copyOp(attOp, opOut);
attOp.opcode = '';
break;
}
}
}
};
Changeset.applyToAttribution = function(cs, astr, pool) {
var unpacked = Changeset.unpack(cs);
return Changeset.applyZip(astr, 0, unpacked.ops, 0, function(op1, op2, opOut) {
return Changeset._slicerZipperFunc(op1, op2, opOut, pool);
});
};
/*Changeset.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) {
var iter = Changeset.opIterator(opsStr, optStartIndex);
var bankIndex = 0;
};*/
Changeset.mutateAttributionLines = function(cs, lines, pool) {
//dmesg(cs);
//dmesg(lines.toSource()+" ->");
var unpacked = Changeset.unpack(cs);
var csIter = Changeset.opIterator(unpacked.ops);
var csBank = unpacked.charBank;
var csBankIndex = 0;
// treat the attribution lines as text lines, mutating a line at a time
var mut = Changeset.textLinesMutator(lines);
var lineIter = null;
function isNextMutOp() {
return (lineIter && lineIter.hasNext()) || mut.hasMore();
}
function nextMutOp(destOp) {
if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) {
var line = mut.removeLines(1);
lineIter = Changeset.opIterator(line);
}
if (lineIter && lineIter.hasNext()) {
lineIter.next(destOp);
}
else {
destOp.opcode = '';
}
}
var lineAssem = null;
function outputMutOp(op) {
//print("outputMutOp: "+op.toSource());
if (! lineAssem) {
lineAssem = Changeset.mergingOpAssembler();
}
lineAssem.append(op);
if (op.lines > 0) {
Changeset.assert(op.lines == 1, "Can't have op.lines of ",op.lines," in attribution lines");
// ship it to the mut
mut.insert(lineAssem.toString(), 1);
lineAssem = null;
}
}
var csOp = Changeset.newOp();
var attOp = Changeset.newOp();
var opOut = Changeset.newOp();
while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) {
if ((! csOp.opcode) && csIter.hasNext()) {
csIter.next(csOp);
}
//print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
//print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null));
//print("csOp: "+csOp.toSource());
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);
//print("skipped: "+csOp.lines);
csOp.opcode = '';
}
else if (csOp.opcode == '+') {
if (csOp.lines > 1) {
var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex;
Changeset.copyOp(csOp, opOut);
csOp.chars -= firstLineLen;
csOp.lines--;
opOut.lines = 1;
opOut.chars = firstLineLen;
}
else {
Changeset.copyOp(csOp, opOut);
csOp.opcode = '';
}
outputMutOp(opOut);
csBankIndex += opOut.chars;
opOut.opcode = '';
}
else {
if ((! attOp.opcode) && isNextMutOp()) {
nextMutOp(attOp);
}
//print("attOp: "+attOp.toSource());
Changeset._slicerZipperFunc(attOp, csOp, opOut, pool);
if (opOut.opcode) {
outputMutOp(opOut);
opOut.opcode = '';
}
}
}
Changeset.assert(! lineAssem, "line assembler not finished");
mut.close();
//dmesg("-> "+lines.toSource());
};
Changeset.joinAttributionLines = function(theAlines) {
var assem = Changeset.mergingOpAssembler();
for(var i=0;i<theAlines.length;i++) {
var aline = theAlines[i];
var iter = Changeset.opIterator(aline);
while (iter.hasNext()) {
assem.append(iter.next());
}
}
return assem.toString();
};
Changeset.splitAttributionLines = function(attrOps, text) {
var iter = Changeset.opIterator(attrOps);
var assem = Changeset.mergingOpAssembler();
var lines = [];
var pos = 0;
function appendOp(op) {
assem.append(op);
if (op.lines > 0) {
lines.push(assem.toString());
assem.clear();
}
pos += op.chars;
}
while (iter.hasNext()) {
var op = iter.next();
var numChars = op.chars;
var numLines = op.lines;
while (numLines > 1) {
var newlineEnd = text.indexOf('\n', pos)+1;
Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines");
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;
};
Changeset.splitTextLines = function(text) {
return text.match(/[^\n]*(?:\n|[^\n]$)/g);
};
Changeset.compose = function(cs1, cs2, pool) {
var unpacked1 = Changeset.unpack(cs1);
var unpacked2 = Changeset.unpack(cs2);
var len1 = unpacked1.oldLen;
var len2 = unpacked1.newLen;
Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition");
var len3 = unpacked2.newLen;
var bankIter1 = Changeset.stringIterator(unpacked1.charBank);
var bankIter2 = Changeset.stringIterator(unpacked2.charBank);
var bankAssem = Changeset.stringAssembler();
var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) {
//var debugBuilder = Changeset.stringAssembler();
//debugBuilder.append(Changeset.opString(op1));
//debugBuilder.append(',');
//debugBuilder.append(Changeset.opString(op2));
//debugBuilder.append(' / ');
var op1code = op1.opcode;
var op2code = op2.opcode;
if (op1code == '+' && op2code == '-') {
bankIter1.skip(Math.min(op1.chars, op2.chars));
}
Changeset._slicerZipperFunc(op1, op2, opOut, pool);
if (opOut.opcode == '+') {
if (op2code == '+') {
bankAssem.append(bankIter2.take(opOut.chars));
}
else {
bankAssem.append(bankIter1.take(opOut.chars));
}
}
//debugBuilder.append(Changeset.opString(op1));
//debugBuilder.append(',');
//debugBuilder.append(Changeset.opString(op2));
//debugBuilder.append(' -> ');
//debugBuilder.append(Changeset.opString(opOut));
//print(debugBuilder.toString());
});
return Changeset.pack(len1, len3, newOps, bankAssem.toString());
};
Changeset.attributeTester = function(attribPair, pool) {
// 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.
if (! pool) {
return never;
}
var attribNum = pool.putAttrib(attribPair, true);
if (attribNum < 0) {
return never;
}
else {
var re = new RegExp('\\*'+Changeset.numToString(attribNum)+
'(?!\\w)');
return function(attribs) {
return re.test(attribs);
};
}
function never(attribs) { return false; }
};
Changeset.identity = function(N) {
return Changeset.pack(N, N, "", "");
};
Changeset.makeSplice = function(oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) {
var oldLen = oldFullText.length;
if (spliceStart >= oldLen) {
spliceStart = oldLen - 1;
}
if (numRemoved > oldFullText.length - spliceStart - 1) {
numRemoved = oldFullText.length - spliceStart - 1;
}
var oldText = oldFullText.substring(spliceStart, spliceStart+numRemoved);
var newLen = oldLen + newText.length - oldText.length;
var assem = Changeset.smartOpAssembler();
assem.appendOpWithText('=', oldFullText.substring(0, spliceStart));
assem.appendOpWithText('-', oldText);
assem.appendOpWithText('+', newText, optNewTextAPairs, pool);
assem.endDocument();
return Changeset.pack(oldLen, newLen, assem.toString(), newText);
};
Changeset.toSplices = function(cs) {
// get a list of splices, [startChar, endChar, newText]
var unpacked = Changeset.unpack(cs);
var splices = [];
var oldPos = 0;
var iter = Changeset.opIterator(unpacked.ops);
var charIter = Changeset.stringIterator(unpacked.charBank);
var inSplice = false;
while (iter.hasNext()) {
var op = iter.next();
if (op.opcode == '=') {
oldPos += op.chars;
inSplice = false;
}
else {
if (! inSplice) {
splices.push([oldPos, oldPos, ""]);
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;
};
Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) {
var newStartChar = startChar;
var newEndChar = endChar;
var splices = Changeset.toSplices(cs);
var lengthChangeSoFar = 0;
for(var i=0;i<splices.length;i++) {
var splice = splices[i];
var spliceStart = splice[0] + lengthChangeSoFar;
var spliceEnd = splice[1] + lengthChangeSoFar;
var newTextLength = splice[2].length;
var thisLengthChange = newTextLength - (spliceEnd - spliceStart);
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];
};
Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) {
// works on changeset or attribution string
var dollarPos = cs.indexOf('$');
if (dollarPos < 0) {
dollarPos = cs.length;
}
var upToDollar = cs.substring(0, dollarPos);
var fromDollar = cs.substring(dollarPos);
// order of attribs stays the same
return upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) {
var oldNum = Changeset.parseNum(a);
var pair = oldPool.getAttrib(oldNum);
var newNum = newPool.putAttrib(pair);
return '*'+Changeset.numToString(newNum);
}) + fromDollar;
};
Changeset.makeAttribution = function(text) {
var assem = Changeset.smartOpAssembler();
assem.appendOpWithText('+', text);
return assem.toString();
};
// callable on a changeset, attribution string, or attribs property of an op
Changeset.eachAttribNumber = function(cs, func) {
var dollarPos = cs.indexOf('$');
if (dollarPos < 0) {
dollarPos = cs.length;
}
var upToDollar = cs.substring(0, dollarPos);
upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) {
func(Changeset.parseNum(a));
return '';
});
};
// callable on a changeset, attribution string, or attribs property of an op,
// though it may easily create adjacent ops that can be merged.
Changeset.filterAttribNumbers = function(cs, filter) {
return Changeset.mapAttribNumbers(cs, filter);
};
Changeset.mapAttribNumbers = function(cs, func) {
var dollarPos = cs.indexOf('$');
if (dollarPos < 0) {
dollarPos = cs.length;
}
var upToDollar = cs.substring(0, dollarPos);
var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function(s, a) {
var n = func(Changeset.parseNum(a));
if (n === true) {
return s;
}
else if ((typeof n) === "number") {
return '*'+Changeset.numToString(n);
}
else {
return '';
}
});
return newUpToDollar + cs.substring(dollarPos);
};
Changeset.makeAText = function(text, attribs) {
return { text: text, attribs: (attribs || Changeset.makeAttribution(text)) };
};
Changeset.applyToAText = function(cs, atext, pool) {
return { text: Changeset.applyToText(cs, atext.text),
attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) };
};
Changeset.cloneAText = function(atext) {
return { text: atext.text, attribs: atext.attribs };
};
Changeset.copyAText = function(atext1, atext2) {
atext2.text = atext1.text;
atext2.attribs = atext1.attribs;
};
Changeset.appendATextToAssembler = function(atext, assem) {
// intentionally skips last newline char of atext
var iter = Changeset.opIterator(atext.attribs);
var op = Changeset.newOp();
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 {
var nextToLastNewlineEnd =
atext.text.lastIndexOf('\n', atext.text.length-2) + 1;
var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1;
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);
}
}
};
Changeset.prepareForWire = function(cs, pool) {
var newPool = new AttribPool();
var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool);
return {translated: newCs, pool: newPool};
};
Changeset.isIdentity = function(cs) {
var unpacked = Changeset.unpack(cs);
return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen;
};
Changeset.opAttributeValue = function(op, key, pool) {
return Changeset.attribsAttributeValue(op.attribs, key, pool);
};
Changeset.attribsAttributeValue = function(attribs, key, pool) {
var value = '';
if (attribs) {
Changeset.eachAttribNumber(attribs, function(n) {
if (pool.getAttribKey(n) == key) {
value = pool.getAttribValue(n);
}
});
}
return value;
};
Changeset.builder = function(oldLen) {
var assem = Changeset.smartOpAssembler();
var o = Changeset.newOp();
var charBank = Changeset.stringAssembler();
var self = {
// attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case)
keep: function(N, L, attribs, pool) {
o.opcode = '=';
o.attribs = (attribs &&
Changeset.makeAttribsString('=', attribs, pool)) || '';
o.chars = N;
o.lines = (L || 0);
assem.append(o);
return self;
},
keepText: function(text, attribs, pool) {
assem.appendOpWithText('=', text, attribs, pool);
return self;
},
insert: function(text, attribs, pool) {
assem.appendOpWithText('+', text, attribs, pool);
charBank.append(text);
return self;
},
remove: function(N, L) {
o.opcode = '-';
o.attribs = '';
o.chars = N;
o.lines = (L || 0);
assem.append(o);
return self;
},
toString: function() {
assem.endDocument();
var newLen = oldLen + assem.getLengthChange();
return Changeset.pack(oldLen, newLen, assem.toString(),
charBank.toString());
}
};
return self;
};
Changeset.makeAttribsString = function(opcode, attribs, pool) {
// makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work
if (! attribs) {
return '';
}
else if ((typeof attribs) == "string") {
return attribs;
}
else if (pool && attribs && attribs.length) {
if (attribs.length > 1) {
attribs = attribs.slice();
attribs.sort();
}
var result = [];
for(var i=0;i<attribs.length;i++) {
var pair = attribs[i];
if (opcode == '=' || (opcode == '+' && pair[1])) {
result.push('*'+Changeset.numToString(pool.putAttrib(pair)));
}
}
return result.join('');
}
};
// like "substring" but on a single-line attribution string
Changeset.subattribution = function(astr, start, optEnd) {
var iter = Changeset.opIterator(astr, 0);
var assem = Changeset.smartOpAssembler();
var attOp = Changeset.newOp();
var csOp = Changeset.newOp();
var opOut = Changeset.newOp();
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++;
}
Changeset._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();
};
Changeset.inverse = function(cs, lines, alines, pool) {
// lines and alines are what the changeset 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 lines_length() {
if ((typeof lines.length) == "number") {
return lines.length;
}
else {
return lines.length();
}
}
function alines_get(idx) {
if (alines.get) {
return alines.get(idx);
}
else {
return alines[idx];
}
}
function alines_length() {
if ((typeof alines.length) == "number") {
return alines.length;
}
else {
return alines.length();
}
}
var curLine = 0;
var curChar = 0;
var curLineOpIter = null;
var curLineOpIterLine;
var curLineNextOp = Changeset.newOp('+');
var unpacked = Changeset.unpack(cs);
var csIter = Changeset.opIterator(unpacked.ops);
var builder = Changeset.builder(unpacked.newLen);
function consumeAttribRuns(numChars, func/*(len, attribs, endsLine)*/) {
if ((! curLineOpIter) || (curLineOpIterLine != curLine)) {
// create curLineOpIter and advance it to curChar
curLineOpIter = Changeset.opIterator(alines_get(curLine));
curLineOpIterLine = curLine;
var indexIntoLine = 0;
var done = false;
while (! done) {
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 = Changeset.opIterator(alines_get(curLine));
}
if (! curLineNextOp.chars) {
curLineOpIter.next(curLineNextOp);
}
var charsToUse = Math.min(numChars, curLineNextOp.chars);
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;
}
else {
if (curLineOpIter && curLineOpIterLine == curLine) {
consumeAttribRuns(N, function() {});
}
else {
curChar += N;
}
}
}
function nextText(numChars) {
var len = 0;
var assem = Changeset.stringAssembler();
var firstString = lines_get(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
var lineNum = curLine+1;
while (len < numChars) {
var nextString = lines_get(lineNum);
len += nextString.length;
assem.append(nextString);
lineNum++;
}
return assem.toString().substring(0, numChars);
}
function cachedStrFunc(func) {
var cache = {};
return function(s) {
if (! cache[s]) {
cache[s] = func(s);
}
return cache[s];
};
}
var attribKeys = [];
var attribValues = [];
while (csIter.hasNext()) {
var csOp = csIter.next();
if (csOp.opcode == '=') {
if (csOp.attribs) {
attribKeys.length = 0;
attribValues.length = 0;
Changeset.eachAttribNumber(csOp.attribs, function(n) {
attribKeys.push(pool.getAttribKey(n));
attribValues.push(pool.getAttribValue(n));
});
var undoBackToAttribs = cachedStrFunc(function(attribs) {
var backAttribs = [];
for(var i=0;i<attribKeys.length;i++) {
var appliedKey = attribKeys[i];
var appliedValue = attribValues[i];
var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, pool);
if (appliedValue != oldValue) {
backAttribs.push([appliedKey, oldValue]);
}
}
return Changeset.makeAttribsString('=', backAttribs, pool);
});
consumeAttribRuns(csOp.chars, function(len, attribs, endsLine) {
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;
consumeAttribRuns(csOp.chars, function(len, attribs, endsLine) {
builder.insert(textBank.substr(textBankIndex, len), attribs);
textBankIndex += len;
});
}
}
return Changeset.checkRep(builder.toString());
};
// %CLIENT FILE ENDS HERE%
Changeset.follow = function(cs1, cs2, reverseInsertOrder, pool) {
var unpacked1 = Changeset.unpack(cs1);
var unpacked2 = Changeset.unpack(cs2);
var len1 = unpacked1.oldLen;
var len2 = unpacked2.oldLen;
Changeset.assert(len1 == len2, "mismatched follow");
var chars1 = Changeset.stringIterator(unpacked1.charBank);
var chars2 = Changeset.stringIterator(unpacked2.charBank);
var oldLen = unpacked1.newLen;
var oldPos = 0;
var newLen = 0;
var hasInsertFirst = Changeset.attributeTester(['insertorder','first'],
pool);
var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) {
if (op1.opcode == '+' || op2.opcode == '+') {
var whichToDo;
if (op2.opcode != '+') {
whichToDo = 1;
}
else if (op1.opcode != '+') {
whichToDo = 2;
}
else {
// both +
var firstChar1 = chars1.peek(1);
var firstChar2 = chars2.peek(1);
var insertFirst1 = hasInsertFirst(op1.attribs);
var insertFirst2 = hasInsertFirst(op2.attribs);
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);
Changeset.copyOp(op2, opOut);
op2.opcode = '';
}
}
else if (op1.opcode == '-') {
if (! op2.opcode) {
op1.opcode = '';
}
else {
if (op1.chars <= op2.chars) {
op2.chars -= op1.chars;
op2.lines -= op1.lines;
op1.opcode = '';
if (! op2.chars) {
op2.opcode = '';
}
}
else {
op1.chars -= op2.chars;
op1.lines -= op2.lines;
op2.opcode = '';
}
}
}
else if (op2.opcode == '-') {
Changeset.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) {
Changeset.copyOp(op2, opOut);
op2.opcode = '';
}
else if (! op2.opcode) {
Changeset.copyOp(op1, opOut);
op1.opcode = '';
}
else {
// both keeps
opOut.opcode = '=';
opOut.attribs = Changeset.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) {
case '=': oldPos += opOut.chars; newLen += opOut.chars; break;
case '-': oldPos += opOut.chars; break;
case '+': newLen += opOut.chars; break;
}
});
newLen += oldLen - oldPos;
return Changeset.pack(oldLen, newLen, newOps, unpacked2.charBank);
};
Changeset.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;
var atts = [];
att2.replace(/\*([0-9a-z]+)/g, function(_, a) {
atts.push(pool.getAttrib(Changeset.parseNum(a)));
return '';
});
att1.replace(/\*([0-9a-z]+)/g, function(_, a) {
var pair1 = pool.getAttrib(Changeset.parseNum(a));
for(var i=0;i<atts.length;i++) {
var pair2 = atts[i];
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
var buf = Changeset.stringAssembler();
for(var i=0;i<atts.length;i++) {
buf.append('*');
buf.append(Changeset.numToString(pool.putAttrib(atts[i])));
}
return buf.toString();
};