diff --git a/src/node/db/API.js b/src/node/db/API.js
index cee63a9e..f99a43af 100644
--- a/src/node/db/API.js
+++ b/src/node/db/API.js
@@ -30,6 +30,7 @@ var async = require("async");
var exportHtml = require("../utils/ExportHtml");
var importHtml = require("../utils/ImportHtml");
var cleanText = require("./Pad").cleanText;
+var PadDiff = require("../utils/padDiff");
/**********************/
/**GROUP FUNCTIONS*****/
@@ -656,6 +657,86 @@ exports.getChatHead = function(padID, callback)
});
}
+/**
+createDiffHTML(padID, startRev, endRev) returns an object of diffs from 2 points in a pad
+
+Example returns:
+
+{"code":0,"message":"ok","data":{"html":"Welcome to Etherpad Lite!
This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!
Get involved with Etherpad at http://etherpad.org
aw
","authors":["a.HKIv23mEbachFYfH",""]}}
+{"code":4,"message":"no or wrong API Key","data":null}
+*/
+exports.createDiffHTML = function(padID, startRev, endRev, callback){
+ //check if rev is a number
+ if(startRev !== undefined && typeof startRev != "number")
+ {
+ //try to parse the number
+ if(!isNaN(parseInt(startRev)))
+ {
+ startRev = parseInt(startRev, 10);
+ }
+ else
+ {
+ callback({stop: "startRev is not a number"});
+ return;
+ }
+ }
+
+ //check if rev is a number
+ if(endRev !== undefined && typeof endRev != "number")
+ {
+ //try to parse the number
+ if(!isNaN(parseInt(endRev)))
+ {
+ endRev = parseInt(endRev, 10);
+ }
+ else
+ {
+ callback({stop: "endRev is not a number"});
+ return;
+ }
+ }
+
+ //get the pad
+ getPadSafe(padID, true, function(err, pad)
+ {
+ if(err){
+ return callback(err);
+ }
+
+ try {
+ var padDiff = new PadDiff(pad, startRev, endRev);
+ } catch(e) {
+ return callback({stop:e.message});
+ }
+ var html, authors;
+
+ async.series([
+ function(callback){
+ padDiff.getHtml(function(err, _html){
+ if(err){
+ return callback(err);
+ }
+
+ html = _html;
+ callback();
+ });
+ },
+ function(callback){
+ padDiff.getAuthors(function(err, _authors){
+ if(err){
+ return callback(err);
+ }
+
+ authors = _authors;
+ callback();
+ });
+ }
+ ], function(err){
+ callback(err, {html: html, authors: authors})
+ });
+ });
+}
+
/******************************/
/** INTERNAL HELPER FUNCTIONS */
/******************************/
diff --git a/src/node/db/AuthorManager.js b/src/node/db/AuthorManager.js
index 28b2dd91..667e0605 100644
--- a/src/node/db/AuthorManager.js
+++ b/src/node/db/AuthorManager.js
@@ -24,6 +24,10 @@ var db = require("./DB").db;
var async = require("async");
var randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString;
+exports.getColorPalette = function(){
+ return ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"];
+};
+
/**
* Checks if the author exists
*/
diff --git a/src/node/db/Pad.js b/src/node/db/Pad.js
index da1ce9e1..4701e82a 100644
--- a/src/node/db/Pad.js
+++ b/src/node/db/Pad.js
@@ -213,6 +213,48 @@ Pad.prototype.getInternalRevisionAText = function getInternalRevisionAText(targe
});
};
+Pad.prototype.getRevision = function getRevisionChangeset(revNum, callback) {
+ db.get("pad:"+this.id+":revs:"+revNum, callback);
+};
+
+Pad.prototype.getAllAuthorColors = function getAllAuthorColors(callback){
+ var authors = this.getAllAuthors();
+ var returnTable = {};
+ var colorPalette = authorManager.getColorPalette();
+
+ async.forEach(authors, function(author, callback){
+ authorManager.getAuthorColorId(author, function(err, colorId){
+ if(err){
+ return callback(err);
+ }
+ //colorId might be a hex color or an number out of the palette
+ returnTable[author]=colorPalette[colorId] || colorId;
+
+ callback();
+ });
+ }, function(err){
+ callback(err, returnTable);
+ });
+};
+
+Pad.prototype.getValidRevisionRange = function getValidRevisionRange(startRev, endRev) {
+ startRev = parseInt(startRev, 10);
+ var head = this.getHeadRevisionNumber();
+ endRev = endRev ? parseInt(endRev, 10) : head;
+ if(isNaN(startRev) || startRev < 0 || startRev > head) {
+ startRev = null;
+ }
+ if(isNaN(endRev) || endRev < startRev) {
+ endRev = null;
+ } else if(endRev > head) {
+ endRev = head;
+ }
+ if(startRev !== null && endRev !== null) {
+ return { startRev: startRev , endRev: endRev }
+ }
+ return null;
+};
+
Pad.prototype.getKeyRevisionNumber = function getKeyRevisionNumber(revNum) {
return Math.floor(revNum / 100) * 100;
};
diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js
index 6085edbf..9f86277a 100644
--- a/src/node/handler/APIHandler.js
+++ b/src/node/handler/APIHandler.js
@@ -180,6 +180,7 @@ var version =
, "deleteGroup" : ["groupID"]
, "listPads" : ["groupID"]
, "listAllPads" : []
+ , "createDiffHTML" : ["padID", "startRev", "endRev"]
, "createPad" : ["padID", "text"]
, "createGroupPad" : ["groupID", "padName", "text"]
, "createAuthor" : ["name"]
diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js
index 434c25ad..24f72c79 100644
--- a/src/node/handler/PadMessageHandler.js
+++ b/src/node/handler/PadMessageHandler.js
@@ -1028,7 +1028,7 @@ function handleClientReady(client, message)
"globalPadId": message.padId,
"time": currentTime,
},
- "colorPalette": ["#ffc7c7", "#fff1c7", "#e3ffc7", "#c7ffd5", "#c7ffff", "#c7d5ff", "#e3c7ff", "#ffc7f1", "#ff8f8f", "#ffe38f", "#c7ff8f", "#8fffab", "#8fffff", "#8fabff", "#c78fff", "#ff8fe3", "#d97979", "#d9c179", "#a9d979", "#79d991", "#79d9d9", "#7991d9", "#a979d9", "#d979c1", "#d9a9a9", "#d9cda9", "#c1d9a9", "#a9d9b5", "#a9d9d9", "#a9b5d9", "#c1a9d9", "#d9a9cd", "#4c9c82", "#12d1ad", "#2d8e80", "#7485c3", "#a091c7", "#3185ab", "#6818b4", "#e6e76d", "#a42c64", "#f386e5", "#4ecc0c", "#c0c236", "#693224", "#b5de6a", "#9b88fd", "#358f9b", "#496d2f", "#e267fe", "#d23056", "#1a1a64", "#5aa335", "#d722bb", "#86dc6c", "#b5a714", "#955b6a", "#9f2985", "#4b81c8", "#3d6a5b", "#434e16", "#d16084", "#af6a0e", "#8c8bd8"],
+ "colorPalette": authorManager.getColorPalette(),
"clientIp": "127.0.0.1",
"userIsGuest": true,
"userColor": authorColorId,
diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js
index 35403013..06919488 100644
--- a/src/node/utils/ExportHtml.js
+++ b/src/node/utils/ExportHtml.js
@@ -1,12 +1,12 @@
/**
* 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.
@@ -91,8 +91,9 @@ function getPadHTML(pad, revNum, callback)
}
exports.getPadHTML = getPadHTML;
+exports.getHTMLFromAtext = getHTMLFromAtext;
-function getHTMLFromAtext(pad, atext)
+function getHTMLFromAtext(pad, atext, authorColors)
{
var apool = pad.apool();
var textLines = atext.text.slice(0, -1).split('\n');
@@ -101,6 +102,42 @@ function getHTMLFromAtext(pad, atext)
var tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
var props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
var anumMap = {};
+ var css = "";
+
+ var stripDotFromAuthorID = function(id){
+ return id.replace(/\./g,'_');
+ };
+
+ if(authorColors){
+ css+="";
+ }
props.forEach(function (propName, i)
{
@@ -125,22 +162,53 @@ function getHTMLFromAtext(pad, atext)
// Just bold Bold and italics Just italics
var taker = Changeset.stringIterator(text);
var assem = Changeset.stringAssembler();
-
var openTags = [];
+
+ function getSpanClassFor(i){
+ //return if author colors are disabled
+ if (!authorColors) return false;
+
+ var property = props[i];
+
+ if(property.substr(0,6) === "author"){
+ return stripDotFromAuthorID(property);
+ }
+
+ if(property === "removed"){
+ return "removed";
+ }
+
+ return false;
+ }
+
function emitOpenTag(i)
{
openTags.unshift(i);
- assem.append('<');
- assem.append(tags[i]);
- assem.append('>');
+ var spanClass = getSpanClassFor(i);
+
+ if(spanClass){
+ assem.append('');
+ } else {
+ assem.append('<');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
}
function emitCloseTag(i)
{
openTags.shift();
- assem.append('');
- assem.append(tags[i]);
- assem.append('>');
+ var spanClass = getSpanClassFor(i);
+
+ if(spanClass){
+ assem.append('');
+ } else {
+ assem.append('');
+ assem.append(tags[i]);
+ assem.append('>');
+ }
}
function orderdCloseTags(tags2close)
@@ -303,7 +371,7 @@ function getHTMLFromAtext(pad, atext)
return _processSpaces(assem.toString());
} // end getLineHTML
- var pieces = [];
+ var pieces = [css];
// Need to deal with constraints imposed on HTML lists; can
// only gain one level of nesting at once, can't change type
diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js
new file mode 100644
index 00000000..1b3cf58f
--- /dev/null
+++ b/src/node/utils/padDiff.js
@@ -0,0 +1,554 @@
+var Changeset = require("../../static/js/Changeset");
+var async = require("async");
+var exportHtml = require('./ExportHtml');
+
+function PadDiff (pad, fromRev, toRev){
+ //check parameters
+ if(!pad || !pad.id || !pad.atext || !pad.pool)
+ {
+ throw new Error('Invalid pad');
+ }
+
+ var range = pad.getValidRevisionRange(fromRev, toRev);
+ if(!range) { throw new Error('Invalid revision range.' +
+ ' startRev: ' + fromRev +
+ ' endRev: ' + toRev); }
+
+ this._pad = pad;
+ this._fromRev = range.startRev;
+ this._toRev = range.endRev;
+ this._html = null;
+ this._authors = [];
+}
+
+PadDiff.prototype._isClearAuthorship = function(changeset){
+ //unpack
+ var unpacked = Changeset.unpack(changeset);
+
+ //check if there is nothing in the charBank
+ if(unpacked.charBank !== "")
+ return false;
+
+ //check if oldLength == newLength
+ if(unpacked.oldLen !== unpacked.newLen)
+ return false;
+
+ //lets iterator over the operators
+ var iterator = Changeset.opIterator(unpacked.ops);
+
+ //get the first operator, this should be a clear operator
+ var clearOperator = iterator.next();
+
+ //check if there is only one operator
+ if(iterator.hasNext() === true)
+ return false;
+
+ //check if this operator doesn't change text
+ if(clearOperator.opcode !== "=")
+ return false;
+
+ //check that this operator applys to the complete text
+ //if the text ends with a new line, its exactly one character less, else it has the same length
+ if(clearOperator.chars !== unpacked.oldLen-1 && clearOperator.chars !== unpacked.oldLen)
+ return false;
+
+ var attributes = [];
+ Changeset.eachAttribNumber(changeset, function(attrNum){
+ attributes.push(attrNum);
+ });
+
+ //check that this changeset uses only one attribute
+ if(attributes.length !== 1)
+ return false;
+
+ var appliedAttribute = this._pad.pool.getAttrib(attributes[0]);
+
+ //check if the applied attribute is an anonymous author attribute
+ if(appliedAttribute[0] !== "author" || appliedAttribute[1] !== "")
+ return false;
+
+ return true;
+}
+
+PadDiff.prototype._createClearAuthorship = function(rev, callback){
+ var self = this;
+ this._pad.getInternalRevisionAText(rev, function(err, atext){
+ if(err){
+ return callback(err);
+ }
+
+ //build clearAuthorship changeset
+ var builder = Changeset.builder(atext.text.length);
+ builder.keepText(atext.text, [['author','']], self._pad.pool);
+ var changeset = builder.toString();
+
+ callback(null, changeset);
+ });
+}
+
+PadDiff.prototype._createClearStartAtext = function(rev, callback){
+ var self = this;
+
+ //get the atext of this revision
+ this._pad.getInternalRevisionAText(rev, function(err, atext){
+ if(err){
+ return callback(err);
+ }
+
+ //create the clearAuthorship changeset
+ self._createClearAuthorship(rev, function(err, changeset){
+ if(err){
+ return callback(err);
+ }
+
+ //apply the clearAuthorship changeset
+ var newAText = Changeset.applyToAText(changeset, atext, self._pad.pool);
+
+ callback(null, newAText);
+ });
+ });
+}
+
+PadDiff.prototype._getChangesetsInBulk = function(startRev, count, callback) {
+ var self = this;
+
+ //find out which revisions we need
+ var revisions = [];
+ for(var i=startRev;i<(startRev+count) && i<=this._pad.head;i++){
+ revisions.push(i);
+ }
+
+ var changesets = [], authors = [];
+
+ //get all needed revisions
+ async.forEach(revisions, function(rev, callback){
+ self._pad.getRevision(rev, function(err, revision){
+ if(err){
+ return callback(err)
+ }
+
+ var arrayNum = rev-startRev;
+
+ changesets[arrayNum] = revision.changeset;
+ authors[arrayNum] = revision.meta.author;
+
+ callback();
+ });
+ }, function(err){
+ callback(err, changesets, authors);
+ });
+}
+
+PadDiff.prototype._addAuthors = function(authors) {
+ var self = this;
+ //add to array if not in the array
+ authors.forEach(function(author){
+ if(self._authors.indexOf(author) == -1){
+ self._authors.push(author);
+ }
+ });
+}
+
+PadDiff.prototype._createDiffAtext = function(callback) {
+ var self = this;
+ var bulkSize = 100;
+
+ //get the cleaned startAText
+ self._createClearStartAtext(self._fromRev, function(err, atext){
+ if(err) { return callback(err); }
+
+ var superChangeset = null;
+
+ var rev = self._fromRev + 1;
+
+ //async while loop
+ async.whilst(
+ //loop condition
+ function () { return rev <= self._toRev; },
+
+ //loop body
+ function (callback) {
+ //get the bulk
+ self._getChangesetsInBulk(rev,bulkSize,function(err, changesets, authors){
+ var addedAuthors = [];
+
+ //run trough all changesets
+ for(var i=0;i= 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 = [];
+
+ //iterate over all operators of this changeset
+ while (csIter.hasNext()) {
+ var csOp = csIter.next();
+
+ if (csOp.opcode == '=') {
+ var textBank = nextText(csOp.chars);
+
+ // decide if this equal operator is an attribution change or not. We can see this by checkinf if attribs is set.
+ // If the text this operator applies to is only a star, than this is a false positive and should be ignored
+ if (csOp.attribs && textBank != "*") {
+ var deletedAttrib = apool.putAttrib(["removed", true]);
+ var authorAttrib = apool.putAttrib(["author", ""]);;
+
+ attribKeys.length = 0;
+ attribValues.length = 0;
+ Changeset.eachAttribNumber(csOp.attribs, function (n) {
+ attribKeys.push(apool.getAttribKey(n));
+ attribValues.push(apool.getAttribValue(n));
+
+ if(apool.getAttribKey(n) === "author"){
+ authorAttrib = 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, apool);
+ if (appliedValue != oldValue) {
+ backAttribs.push([appliedKey, oldValue]);
+ }
+ }
+ return Changeset.makeAttribsString('=', backAttribs, apool);
+ });
+
+ var oldAttribsAddition = "*" + Changeset.numToString(deletedAttrib) + "*" + Changeset.numToString(authorAttrib);
+
+ var textLeftToProcess = textBank;
+
+ while(textLeftToProcess.length > 0){
+ //process till the next line break or process only one line break
+ var lengthToProcess = textLeftToProcess.indexOf("\n");
+ var lineBreak = false;
+ switch(lengthToProcess){
+ case -1:
+ lengthToProcess=textLeftToProcess.length;
+ break;
+ case 0:
+ lineBreak = true;
+ lengthToProcess=1;
+ break;
+ }
+
+ //get the text we want to procceed in this step
+ var processText = textLeftToProcess.substr(0, lengthToProcess);
+ textLeftToProcess = textLeftToProcess.substr(lengthToProcess);
+
+ if(lineBreak){
+ builder.keep(1, 1); //just skip linebreaks, don't do a insert + keep for a linebreak
+
+ //consume the attributes of this linebreak
+ consumeAttribRuns(1, function(){});
+ } else {
+ //add the old text via an insert, but add a deletion attribute + the author attribute of the author who deleted it
+ var textBankIndex = 0;
+ consumeAttribRuns(lengthToProcess, function (len, attribs, endsLine) {
+ //get the old attributes back
+ var attribs = (undoBackToAttribs(attribs) || "") + oldAttribsAddition;
+
+ builder.insert(processText.substr(textBankIndex, len), attribs);
+ textBankIndex += len;
+ });
+
+ builder.keep(lengthToProcess, 0);
+ }
+ }
+ } else {
+ skip(csOp.chars, csOp.lines);
+ builder.keep(csOp.chars, csOp.lines);
+ }
+ } else if (csOp.opcode == '+') {
+ builder.keep(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 + csOp.attribs);
+ textBankIndex += len;
+ });
+ }
+ }
+
+ return Changeset.checkRep(builder.toString());
+};
+
+//export the constructor
+module.exports = PadDiff;
diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js
index cfea4362..b1604212 100644
--- a/src/static/js/Changeset.js
+++ b/src/static/js/Changeset.js
@@ -2182,3 +2182,121 @@ exports.followAttributes = function (att1, att2, pool) {
}
return buf.toString();
};
+
+exports.composeWithDeletions = function (cs1, cs2, pool) {
+ var unpacked1 = exports.unpack(cs1);
+ var unpacked2 = exports.unpack(cs2);
+ var len1 = unpacked1.oldLen;
+ var len2 = unpacked1.newLen;
+ exports.assert(len2 == unpacked2.oldLen, "mismatched composition");
+ var len3 = unpacked2.newLen;
+ var bankIter1 = exports.stringIterator(unpacked1.charBank);
+ var bankIter2 = exports.stringIterator(unpacked2.charBank);
+ var bankAssem = exports.stringAssembler();
+
+ var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) {
+ var op1code = op1.opcode;
+ var op2code = op2.opcode;
+ if (op1code == '+' && op2code == '-') {
+ bankIter1.skip(Math.min(op1.chars, op2.chars));
+ }
+ exports._slicerZipperFuncWithDeletions(op1, op2, opOut, pool);
+ if (opOut.opcode == '+') {
+ if (op2code == '+') {
+ bankAssem.append(bankIter2.take(opOut.chars));
+ } else {
+ bankAssem.append(bankIter1.take(opOut.chars));
+ }
+ }
+ });
+
+ return exports.pack(len1, len3, newOps, bankAssem.toString());
+};
+
+// This function is 95% like _slicerZipperFunc, we just changed two lines to ensure it merges the attribs of deletions properly.
+// This is necassary for correct paddiff. But to ensure these changes doesn't affect anything else, we've created a seperate function only used for paddiffs
+exports._slicerZipperFuncWithDeletions= function (attOp, csOp, opOut, pool) {
+ // attOp is the op from the sequence that is being operated on, either an
+ // attribution string or the earlier of two exportss being composed.
+ // pool can be null if definitely not needed.
+ //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
+ if (attOp.opcode == '-') {
+ exports.copyOp(attOp, opOut);
+ attOp.opcode = '';
+ } else if (!attOp.opcode) {
+ exports.copyOp(csOp, opOut);
+ csOp.opcode = '';
+ } else {
+ switch (csOp.opcode) {
+ 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 = csOp.attribs; //changed by yammer
+ }
+ attOp.chars -= csOp.chars;
+ attOp.lines -= csOp.lines;
+ csOp.opcode = '';
+ if (!attOp.chars) {
+ attOp.opcode = '';
+ }
+ } else {
+ // delete and keep going
+ if (attOp.opcode == '=') {
+ opOut.opcode = '-';
+ opOut.chars = attOp.chars;
+ opOut.lines = attOp.lines;
+ opOut.attribs = csOp.attribs; //changed by yammer
+ }
+ csOp.chars -= attOp.chars;
+ csOp.lines -= attOp.lines;
+ attOp.opcode = '';
+ }
+ break;
+ }
+ case '+':
+ {
+ // insert
+ exports.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 = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
+ csOp.opcode = '';
+ attOp.chars -= csOp.chars;
+ attOp.lines -= csOp.lines;
+ if (!attOp.chars) {
+ attOp.opcode = '';
+ }
+ } else {
+ // keep and keep going
+ opOut.opcode = attOp.opcode;
+ opOut.chars = attOp.chars;
+ opOut.lines = attOp.lines;
+ opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
+ attOp.opcode = '';
+ csOp.chars -= attOp.chars;
+ csOp.lines -= attOp.lines;
+ }
+ break;
+ }
+ case '':
+ {
+ exports.copyOp(attOp, opOut);
+ attOp.opcode = '';
+ break;
+ }
+ }
+ }
+};