// 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 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"); 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 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= 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= 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