2270 lines
52 KiB
JavaScript
2270 lines
52 KiB
JavaScript
|
// DO NOT EDIT THIS FILE, edit infrastructure/ace/www/easysync2.js
|
||
|
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.easysync2
|
||
|
/**
|
||
|
* 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());
|
||
|
};
|