      1 /**
      2  * Copyright 2009 Google Inc., 2011 Peter 'Pita' Martischka
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS-IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16
     17 var AttributePoolFactory = require("./AttributePoolFactory");
     18
     19 var _opt = null;
     20
     21 //var exports = {};
     22 exports.error = function error(msg) {
     23   var e = new Error(msg);
     24   e.easysync = true;
     25   throw e;
     26 };
     27 exports.assert = function assert(b, msgParts) {
     28   if (!b) {
     29     var msg = Array.prototype.slice.call(arguments, 1).join('');
     30     exports.error("exports: " + msg);
     31   }
     32 };
     33
     34 exports.parseNum = function (str) {
     35   return parseInt(str, 36);
     36 };
     37 exports.numToString = function (num) {
     38   return num.toString(36).toLowerCase();
     39 };
     40 exports.toBaseTen = function (cs) {
     41   var dollarIndex = cs.indexOf('$');
     42   var beforeDollar = cs.substring(0, dollarIndex);
     43   var fromDollar = cs.substring(dollarIndex);
     44   return beforeDollar.replace(/[0-9a-z]+/g, function (s) {
     45     return String(exports.parseNum(s));
     46   }) + fromDollar;
     47 };
     48
     49 exports.oldLen = function (cs) {
     50   return exports.unpack(cs).oldLen;
     51 };
     52 exports.newLen = function (cs) {
     53   return exports.unpack(cs).newLen;
     54 };
     55
     56 exports.opIterator = function (opsStr, optStartIndex) {
     57   //print(opsStr);
     58   var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g;
     59   var startIndex = (optStartIndex || 0);
     60   var curIndex = startIndex;
     61   var prevIndex = curIndex;
     62
     63   function nextRegexMatch() {
     64     prevIndex = curIndex;
     65     var result;
     66     if (_opt) {
     67       result = _opt.nextOpInString(opsStr, curIndex);
     68       if (result) {
     69         if (result.opcode() == '?') {
     70           exports.error("Hit error opcode in op stream");
     71         }
     72         curIndex = result.lastIndex();
     73       }
     74     } else {
     75       regex.lastIndex = curIndex;
     76       result = regex.exec(opsStr);
     77       curIndex = regex.lastIndex;
     78       if (result[0] == '?') {
     79         exports.error("Hit error opcode in op stream");
     80       }
     81     }
     82     return result;
     83   }
     84   var regexResult = nextRegexMatch();
     85   var obj = exports.newOp();
     86
     87   function next(optObj) {
     88     var op = (optObj || obj);
     89     if (_opt && regexResult) {
     90       op.attribs = regexResult.attribs();
     91       op.lines = regexResult.lines();
     92       op.chars = regexResult.chars();
     93       op.opcode = regexResult.opcode();
     94       regexResult = nextRegexMatch();
     95     } else if ((!_opt) && regexResult[0]) {
     96       op.attribs = regexResult[1];
     97       op.lines = exports.parseNum(regexResult[2] || 0);
     98       op.opcode = regexResult[3];
     99       op.chars = exports.parseNum(regexResult[4]);
    100       regexResult = nextRegexMatch();
    101     } else {
    102       exports.clearOp(op);
    103     }
    104     return op;
    105   }
    106
    107   function hasNext() {
    108     return !!(_opt ? regexResult : regexResult[0]);
    109   }
    110
    111   function lastIndex() {
    112     return prevIndex;
    113   }
    114   return {
    115     next: next,
    116     hasNext: hasNext,
    117     lastIndex: lastIndex
    118   };
    119 };
    120
    121 exports.clearOp = function (op) {
    122   op.opcode = '';
    123   op.chars = 0;
    124   op.lines = 0;
    125   op.attribs = '';
    126 };
    127 exports.newOp = function (optOpcode) {
    128   return {
    129     opcode: (optOpcode || ''),
    130     chars: 0,
    131     lines: 0,
    132     attribs: ''
    133   };
    134 };
    135 exports.cloneOp = function (op) {
    136   return {
    137     opcode: op.opcode,
    138     chars: op.chars,
    139     lines: op.lines,
    140     attribs: op.attribs
    141   };
    142 };
    143 exports.copyOp = function (op1, op2) {
    144   op2.opcode = op1.opcode;
    145   op2.chars = op1.chars;
    146   op2.lines = op1.lines;
    147   op2.attribs = op1.attribs;
    148 };
    149 exports.opString = function (op) {
    150   // just for debugging
    151   if (!op.opcode) return 'null';
    152   var assem = exports.opAssembler();
    153   assem.append(op);
    154   return assem.toString();
    155 };
    156 exports.stringOp = function (str) {
    157   // just for debugging
    158   return exports.opIterator(str).next();
    159 };
    160
    161 exports.checkRep = function (cs) {
    162   // doesn't check things that require access to attrib pool (e.g. attribute order)
    163   // or original string (e.g. newline positions)
    164   var unpacked = exports.unpack(cs);
    165   var oldLen = unpacked.oldLen;
    166   var newLen = unpacked.newLen;
    167   var ops = unpacked.ops;
    168   var charBank = unpacked.charBank;
    169
    170   var assem = exports.smartOpAssembler();
    171   var oldPos = 0;
    172   var calcNewLen = 0;
    173   var numInserted = 0;
    174   var iter = exports.opIterator(ops);
    175   while (iter.hasNext()) {
    176     var o = iter.next();
    177     switch (o.opcode) {
    178     case '=':
    179       oldPos += o.chars;
    180       calcNewLen += o.chars;
    181       break;
    182     case '-':
    183       oldPos += o.chars;
    184       exports.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs);
    185       break;
    186     case '+':
    187       {
    188         calcNewLen += o.chars;
    189         numInserted += o.chars;
    190         exports.assert(calcNewLen < newLen, calcNewLen, " >= ", newLen, " in ", cs);
    191         break;
    192       }
    193     }
    194     assem.append(o);
    195   }
    196
    197   calcNewLen += oldLen - oldPos;
    198   charBank = charBank.substring(0, numInserted);
    199   while (charBank.length < numInserted) {
    200     charBank += "?";
    201   }
    202
    203   assem.endDocument();
    204   var normalized = exports.pack(oldLen, calcNewLen, assem.toString(), charBank);
    205   exports.assert(normalized == cs, normalized, ' != ', cs);
    206
    207   return cs;
    208 }
    209
    210 exports.smartOpAssembler = function () {
    211   // Like opAssembler but able to produce conforming exportss
    212   // from slightly looser input, at the cost of speed.
    213   // Specifically:
    214   // - merges consecutive operations that can be merged
    215   // - strips final "="
    216   // - ignores 0-length changes
    217   // - reorders consecutive + and - (which margingOpAssembler doesn't do)
    218   var minusAssem = exports.mergingOpAssembler();
    219   var plusAssem = exports.mergingOpAssembler();
    220   var keepAssem = exports.mergingOpAssembler();
    221   var assem = exports.stringAssembler();
    222   var lastOpcode = '';
    223   var lengthChange = 0;
    224
    225   function flushKeeps() {
    226     assem.append(keepAssem.toString());
    227     keepAssem.clear();
    228   }
    229
    230   function flushPlusMinus() {
    231     assem.append(minusAssem.toString());
    232     minusAssem.clear();
    233     assem.append(plusAssem.toString());
    234     plusAssem.clear();
    235   }
    236
    237   function append(op) {
    238     if (!op.opcode) return;
    239     if (!op.chars) return;
    240
    241     if (op.opcode == '-') {
    242       if (lastOpcode == '=') {
    243         flushKeeps();
    244       }
    245       minusAssem.append(op);
    246       lengthChange -= op.chars;
    247     } else if (op.opcode == '+') {
    248       if (lastOpcode == '=') {
    249         flushKeeps();
    250       }
    251       plusAssem.append(op);
    252       lengthChange += op.chars;
    253     } else if (op.opcode == '=') {
    254       if (lastOpcode != '=') {
    255         flushPlusMinus();
    256       }
    257       keepAssem.append(op);
    258     }
    259     lastOpcode = op.opcode;
    260   }
    261
    262   function appendOpWithText(opcode, text, attribs, pool) {
    263     var op = exports.newOp(opcode);
    264     op.attribs = exports.makeAttribsString(opcode, attribs, pool);
    265     var lastNewlinePos = text.lastIndexOf('\n');
    266     if (lastNewlinePos < 0) {
    267       op.chars = text.length;
    268       op.lines = 0;
    269       append(op);
    270     } else {
    271       op.chars = lastNewlinePos + 1;
    272       op.lines = text.match(/\n/g).length;
    273       append(op);
    274       op.chars = text.length - (lastNewlinePos + 1);
    275       op.lines = 0;
    276       append(op);
    277     }
    278   }
    279
    280   function toString() {
    281     flushPlusMinus();
    282     flushKeeps();
    283     return assem.toString();
    284   }
    285
    286   function clear() {
    287     minusAssem.clear();
    288     plusAssem.clear();
    289     keepAssem.clear();
    290     assem.clear();
    291     lengthChange = 0;
    292   }
    293
    294   function endDocument() {
    295     keepAssem.endDocument();
    296   }
    297
    298   function getLengthChange() {
    299     return lengthChange;
    300   }
    301
    302   return {
    303     append: append,
    304     toString: toString,
    305     clear: clear,
    306     endDocument: endDocument,
    307     appendOpWithText: appendOpWithText,
    308     getLengthChange: getLengthChange
    309   };
    310 };
    311
    312 if (_opt) {
    313   exports.mergingOpAssembler = function () {
    314     var assem = _opt.mergingOpAssembler();
    315
    316     function append(op) {
    317       assem.append(op.opcode, op.chars, op.lines, op.attribs);
    318     }
    319
    320     function toString() {
    321       return assem.toString();
    322     }
    323
    324     function clear() {
    325       assem.clear();
    326     }
    327
    328     function endDocument() {
    329       assem.endDocument();
    330     }
    331
    332     return {
    333       append: append,
    334       toString: toString,
    335       clear: clear,
    336       endDocument: endDocument
    337     };
    338   };
    339 } else {
    340   exports.mergingOpAssembler = function () {
    341     // This assembler can be used in production; it efficiently
    342     // merges consecutive operations that are mergeable, ignores
    343     // no-ops, and drops final pure "keeps".  It does not re-order
    344     // operations.
    345     var assem = exports.opAssembler();
    346     var bufOp = exports.newOp();
    347
    348     // If we get, for example, insertions [xxx\n,yyy], those don't merge,
    349     // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
    350     // This variable stores the length of yyy and any other newline-less
    351     // ops immediately after it.
    352     var bufOpAdditionalCharsAfterNewline = 0;
    353
    354     function flush(isEndDocument) {
    355       if (bufOp.opcode) {
    356         if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) {
    357           // final merged keep, leave it implicit
    358         } else {
    359           assem.append(bufOp);
    360           if (bufOpAdditionalCharsAfterNewline) {
    361             bufOp.chars = bufOpAdditionalCharsAfterNewline;
    362             bufOp.lines = 0;
    363             assem.append(bufOp);
    364             bufOpAdditionalCharsAfterNewline = 0;
    365           }
    366         }
    367         bufOp.opcode = '';
    368       }
    369     }
    370
    371     function append(op) {
    372       if (op.chars > 0) {
    373         if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) {
    374           if (op.lines > 0) {
    375             // bufOp and additional chars are all mergeable into a multi-line op
    376             bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars;
    377             bufOp.lines += op.lines;
    378             bufOpAdditionalCharsAfterNewline = 0;
    379           } else if (bufOp.lines == 0) {
    380             // both bufOp and op are in-line
    381             bufOp.chars += op.chars;
    382           } else {
    383             // append in-line text to multi-line bufOp
    384             bufOpAdditionalCharsAfterNewline += op.chars;
    385           }
    386         } else {
    387           flush();
    388           exports.copyOp(op, bufOp);
    389         }
    390       }
    391     }
    392
    393     function endDocument() {
    394       flush(true);
    395     }
    396
    397     function toString() {
    398       flush();
    399       return assem.toString();
    400     }
    401
    402     function clear() {
    403       assem.clear();
    404       exports.clearOp(bufOp);
    405     }
    406     return {
    407       append: append,
    408       toString: toString,
    409       clear: clear,
    410       endDocument: endDocument
    411     };
    412   };
    413 }
    414
    415 if (_opt) {
    416   exports.opAssembler = function () {
    417     var assem = _opt.opAssembler();
    418     // this function allows op to be mutated later (doesn't keep a ref)
    419
    420     function append(op) {
    421       assem.append(op.opcode, op.chars, op.lines, op.attribs);
    422     }
    423
    424     function toString() {
    425       return assem.toString();
    426     }
    427
    428     function clear() {
    429       assem.clear();
    430     }
    431     return {
    432       append: append,
    433       toString: toString,
    434       clear: clear
    435     };
    436   };
    437 } else {
    438   exports.opAssembler = function () {
    439     var pieces = [];
    440     // this function allows op to be mutated later (doesn't keep a ref)
    441
    442     function append(op) {
    443       pieces.push(op.attribs);
    444       if (op.lines) {
    445         pieces.push('|', exports.numToString(op.lines));
    446       }
    447       pieces.push(op.opcode);
    448       pieces.push(exports.numToString(op.chars));
    449     }
    450
    451     function toString() {
    452       return pieces.join('');
    453     }
    454
    455     function clear() {
    456       pieces.length = 0;
    457     }
    458     return {
    459       append: append,
    460       toString: toString,
    461       clear: clear
    462     };
    463   };
    464 }
    465
    466 exports.stringIterator = function (str) {
    467   var curIndex = 0;
    468
    469   function assertRemaining(n) {
    470     exports.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")");
    471   }
    472
    473   function take(n) {
    474     assertRemaining(n);
    475     var s = str.substr(curIndex, n);
    476     curIndex += n;
    477     return s;
    478   }
    479
    480   function peek(n) {
    481     assertRemaining(n);
    482     var s = str.substr(curIndex, n);
    483     return s;
    484   }
    485
    486   function skip(n) {
    487     assertRemaining(n);
    488     curIndex += n;
    489   }
    490
    491   function remaining() {
    492     return str.length - curIndex;
    493   }
    494   return {
    495     take: take,
    496     skip: skip,
    497     remaining: remaining,
    498     peek: peek
    499   };
    500 };
    501
    502 exports.stringAssembler = function () {
    503   var pieces = [];
    504
    505   function append(x) {
    506     pieces.push(String(x));
    507   }
    508
    509   function toString() {
    510     return pieces.join('');
    511   }
    512   return {
    513     append: append,
    514     toString: toString
    515   };
    516 };
    517
    518 // "lines" need not be an array as long as it supports certain calls (lines_foo inside).
    519 exports.textLinesMutator = function (lines) {
    520   // Mutates lines, an array of strings, in place.
    521   // Mutation operations have the same constraints as exports operations
    522   // with respect to newlines, but not the other additional constraints
    523   // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline).
    524   // Can be used to mutate lists of strings where the last char of each string
    525   // is not actually a newline, but for the purposes of N and L values,
    526   // the caller should pretend it is, and for things to work right in that case, the input
    527   // to insert() should be a single line with no newlines.
    528   var curSplice = [0, 0];
    529   var inSplice = false;
    530   // position in document after curSplice is applied:
    531   var curLine = 0,
    532       curCol = 0;
    533   // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
    534   //            curLine >= curSplice[0]
    535   // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
    536   //            curCol == 0
    537
    538   function lines_applySplice(s) {
    539     lines.splice.apply(lines, s);
    540   }
    541
    542   function lines_toSource() {
    543     return lines.toSource();
    544   }
    545
    546   function lines_get(idx) {
    547     if (lines.get) {
    548       return lines.get(idx);
    549     } else {
    550       return lines[idx];
    551     }
    552   }
    553   // can be unimplemented if removeLines's return value not needed
    554
    555   function lines_slice(start, end) {
    556     if (lines.slice) {
    557       return lines.slice(start, end);
    558     } else {
    559       return [];
    560     }
    561   }
    562
    563   function lines_length() {
    564     if ((typeof lines.length) == "number") {
    565       return lines.length;
    566     } else {
    567       return lines.length();
    568     }
    569   }
    570
    571   function enterSplice() {
    572     curSplice[0] = curLine;
    573     curSplice[1] = 0;
    574     if (curCol > 0) {
    575       putCurLineInSplice();
    576     }
    577     inSplice = true;
    578   }
    579
    580   function leaveSplice() {
    581     lines_applySplice(curSplice);
    582     curSplice.length = 2;
    583     curSplice[0] = curSplice[1] = 0;
    584     inSplice = false;
    585   }
    586
    587   function isCurLineInSplice() {
    588     return (curLine - curSplice[0] < (curSplice.length - 2));
    589   }
    590
    591   function debugPrint(typ) {
    592     print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource());
    593   }
    594
    595   function putCurLineInSplice() {
    596     if (!isCurLineInSplice()) {
    597       curSplice.push(lines_get(curSplice[0] + curSplice[1]));
    598       curSplice[1]++;
    599     }
    600     return 2 + curLine - curSplice[0];
    601   }
    602
    603   function skipLines(L, includeInSplice) {
    604     if (L) {
    605       if (includeInSplice) {
    606         if (!inSplice) {
    607           enterSplice();
    608         }
    609         for (var i = 0; i < L; i++) {
    610           curCol = 0;
    611           putCurLineInSplice();
    612           curLine++;
    613         }
    614       } else {
    615         if (inSplice) {
    616           if (L > 1) {
    617             leaveSplice();
    618           } else {
    619             putCurLineInSplice();
    620           }
    621         }
    622         curLine += L;
    623         curCol = 0;
    624       }
    625       //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length);
    626 /*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) {
    627 	  print("BLAH");
    628 	  putCurLineInSplice();
    629 	}*/
    630       // tests case foo in remove(), which isn't otherwise covered in current impl
    631     }
    632     //debugPrint("skip");
    633   }
    634
    635   function skip(N, L, includeInSplice) {
    636     if (N) {
    637       if (L) {
    638         skipLines(L, includeInSplice);
    639       } else {
    640         if (includeInSplice && !inSplice) {
    641           enterSplice();
    642         }
    643         if (inSplice) {
    644           putCurLineInSplice();
    645         }
    646         curCol += N;
    647         //debugPrint("skip");
    648       }
    649     }
    650   }
    651
    652   function removeLines(L) {
    653     var removed = '';
    654     if (L) {
    655       if (!inSplice) {
    656         enterSplice();
    657       }
    658
    659       function nextKLinesText(k) {
    660         var m = curSplice[0] + curSplice[1];
    661         return lines_slice(m, m + k).join('');
    662       }
    663       if (isCurLineInSplice()) {
    664         //print(curCol);
    665         if (curCol == 0) {
    666           removed = curSplice[curSplice.length - 1];
    667           // print("FOO"); // case foo
    668           curSplice.length--;
    669           removed += nextKLinesText(L - 1);
    670           curSplice[1] += L - 1;
    671         } else {
    672           removed = nextKLinesText(L - 1);
    673           curSplice[1] += L - 1;
    674           var sline = curSplice.length - 1;
    675           removed = curSplice[sline].substring(curCol) + removed;
    676           curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]);
    677           curSplice[1] += 1;
    678         }
    679       } else {
    680         removed = nextKLinesText(L);
    681         curSplice[1] += L;
    682       }
    683       //debugPrint("remove");
    684     }
    685     return removed;
    686   }
    687
    688   function remove(N, L) {
    689     var removed = '';
    690     if (N) {
    691       if (L) {
    692         return removeLines(L);
    693       } else {
    694         if (!inSplice) {
    695           enterSplice();
    696         }
    697         var sline = putCurLineInSplice();
    698         removed = curSplice[sline].substring(curCol, curCol + N);
    699         curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N);
    700         //debugPrint("remove");
    701       }
    702     }
    703     return removed;
    704   }
    705
    706   function insert(text, L) {
    707     if (text) {
    708       if (!inSplice) {
    709         enterSplice();
    710       }
    711       if (L) {
    712         var newLines = exports.splitTextLines(text);
    713         if (isCurLineInSplice()) {
    714           //if (curCol == 0) {
    715           //curSplice.length--;
    716           //curSplice[1]--;
    717           //Array.prototype.push.apply(curSplice, newLines);
    718           //curLine += newLines.length;
    719           //}
    720           //else {
    721           var sline = curSplice.length - 1;
    722           var theLine = curSplice[sline];
    723           var lineCol = curCol;
    724           curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
    725           curLine++;
    726           newLines.splice(0, 1);
    727           Array.prototype.push.apply(curSplice, newLines);
    728           curLine += newLines.length;
    729           curSplice.push(theLine.substring(lineCol));
    730           curCol = 0;
    731           //}
    732         } else {
    733           Array.prototype.push.apply(curSplice, newLines);
    734           curLine += newLines.length;
    735         }
    736       } else {
    737         var sline = putCurLineInSplice();
    738         curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol);
    739         curCol += text.length;
    740       }
    741       //debugPrint("insert");
    742     }
    743   }
    744
    745   function hasMore() {
    746     //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]);
    747     var docLines = lines_length();
    748     if (inSplice) {
    749       docLines += curSplice.length - 2 - curSplice[1];
    750     }
    751     return curLine < docLines;
    752   }
    753
    754   function close() {
    755     if (inSplice) {
    756       leaveSplice();
    757     }
    758     //debugPrint("close");
    759   }
    760
    761   var self = {
    762     skip: skip,
    763     remove: remove,
    764     insert: insert,
    765     close: close,
    766     hasMore: hasMore,
    767     removeLines: removeLines,
    768     skipLines: skipLines
    769   };
    770   return self;
    771 };
    772
    773 exports.applyZip = function (in1, idx1, in2, idx2, func) {
    774   var iter1 = exports.opIterator(in1, idx1);
    775   var iter2 = exports.opIterator(in2, idx2);
    776   var assem = exports.smartOpAssembler();
    777   var op1 = exports.newOp();
    778   var op2 = exports.newOp();
    779   var opOut = exports.newOp();
    780   while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) {
    781     if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1);
    782     if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2);
    783     func(op1, op2, opOut);
    784     if (opOut.opcode) {
    785       //print(opOut.toSource());
    786       assem.append(opOut);
    787       opOut.opcode = '';
    788     }
    789   }
    790   assem.endDocument();
    791   return assem.toString();
    792 };
    793
    794 exports.unpack = function (cs) {
    795   var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/;
    796   var headerMatch = headerRegex.exec(cs);
    797   if ((!headerMatch) || (!headerMatch[0])) {
    798     exports.error("Not a exports: " + cs);
    799   }
    800   var oldLen = exports.parseNum(headerMatch[1]);
    801   var changeSign = (headerMatch[2] == '>') ? 1 : -1;
    802   var changeMag = exports.parseNum(headerMatch[3]);
    803   var newLen = oldLen + changeSign * changeMag;
    804   var opsStart = headerMatch[0].length;
    805   var opsEnd = cs.indexOf("$");
    806   if (opsEnd < 0) opsEnd = cs.length;
    807   return {
    808     oldLen: oldLen,
    809     newLen: newLen,
    810     ops: cs.substring(opsStart, opsEnd),
    811     charBank: cs.substring(opsEnd + 1)
    812   };
    813 };
    814
    815 exports.pack = function (oldLen, newLen, opsStr, bank) {
    816   var lenDiff = newLen - oldLen;
    817   var lenDiffStr = (lenDiff >= 0 ? '>' + exports.numToString(lenDiff) : '<' + exports.numToString(-lenDiff));
    818   var a = [];
    819   a.push('Z:', exports.numToString(oldLen), lenDiffStr, opsStr, '$', bank);
    820   return a.join('');
    821 };
    822
    823 exports.applyToText = function (cs, str) {
    824   var unpacked = exports.unpack(cs);
    825   exports.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen);
    826   var csIter = exports.opIterator(unpacked.ops);
    827   var bankIter = exports.stringIterator(unpacked.charBank);
    828   var strIter = exports.stringIterator(str);
    829   var assem = exports.stringAssembler();
    830   while (csIter.hasNext()) {
    831     var op = csIter.next();
    832     switch (op.opcode) {
    833     case '+':
    834       assem.append(bankIter.take(op.chars));
    835       break;
    836     case '-':
    837       strIter.skip(op.chars);
    838       break;
    839     case '=':
    840       assem.append(strIter.take(op.chars));
    841       break;
    842     }
    843   }
    844   assem.append(strIter.take(strIter.remaining()));
    845   return assem.toString();
    846 };
    847
    848 exports.mutateTextLines = function (cs, lines) {
    849   var unpacked = exports.unpack(cs);
    850   var csIter = exports.opIterator(unpacked.ops);
    851   var bankIter = exports.stringIterator(unpacked.charBank);
    852   var mut = exports.textLinesMutator(lines);
    853   while (csIter.hasNext()) {
    854     var op = csIter.next();
    855     switch (op.opcode) {
    856     case '+':
    857       mut.insert(bankIter.take(op.chars), op.lines);
    858       break;
    859     case '-':
    860       mut.remove(op.chars, op.lines);
    861       break;
    862     case '=':
    863       mut.skip(op.chars, op.lines, ( !! op.attribs));
    864       break;
    865     }
    866   }
    867   mut.close();
    868 };
    869
    870 exports.composeAttributes = function (att1, att2, resultIsMutation, pool) {
    871   // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean.
    872   // Sometimes attribute (key,value) pairs are treated as attribute presence
    873   // information, while other times they are treated as operations that
    874   // mutate a set of attributes, and this affects whether an empty value
    875   // is a deletion or a change.
    876   // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result
    877   // ([], [(bold, )], true) -> [(bold, )]
    878   // ([], [(bold, )], false) -> []
    879   // ([], [(bold, true)], true) -> [(bold, true)]
    880   // ([], [(bold, true)], false) -> [(bold, true)]
    881   // ([(bold, true)], [(bold, )], true) -> [(bold, )]
    882   // ([(bold, true)], [(bold, )], false) -> []
    883   // pool can be null if att2 has no attributes.
    884   if ((!att1) && resultIsMutation) {
    885     // In the case of a mutation (i.e. composing two exportss),
    886     // an att2 composed with an empy att1 is just att2.  If att1
    887     // is part of an attribution string, then att2 may remove
    888     // attributes that are already gone, so don't do this optimization.
    889     return att2;
    890   }
    891   if (!att2) return att1;
    892   var atts = [];
    893   att1.replace(/\*([0-9a-z]+)/g, function (_, a) {
    894     atts.push(pool.getAttrib(exports.parseNum(a)));
    895     return '';
    896   });
    897   att2.replace(/\*([0-9a-z]+)/g, function (_, a) {
    898     var pair = pool.getAttrib(exports.parseNum(a));
    899     var found = false;
    900     for (var i = 0; i < atts.length; i++) {
    901       var oldPair = atts[i];
    902       if (oldPair[0] == pair[0]) {
    903         if (pair[1] || resultIsMutation) {
    904           oldPair[1] = pair[1];
    905         } else {
    906           atts.splice(i, 1);
    907         }
    908         found = true;
    909         break;
    910       }
    911     }
    912     if ((!found) && (pair[1] || resultIsMutation)) {
    913       atts.push(pair);
    914     }
    915     return '';
    916   });
    917   atts.sort();
    918   var buf = exports.stringAssembler();
    919   for (var i = 0; i < atts.length; i++) {
    920     buf.append('*');
    921     buf.append(exports.numToString(pool.putAttrib(atts[i])));
    922   }
    923   //print(att1+" / "+att2+" / "+buf.toString());
    924   return buf.toString();
    925 };
    926
    927 exports._slicerZipperFunc = function (attOp, csOp, opOut, pool) {
    928   // attOp is the op from the sequence that is being operated on, either an
    929   // attribution string or the earlier of two exportss being composed.
    930   // pool can be null if definitely not needed.
    931   //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
    932   if (attOp.opcode == '-') {
    933     exports.copyOp(attOp, opOut);
    934     attOp.opcode = '';
    935   } else if (!attOp.opcode) {
    936     exports.copyOp(csOp, opOut);
    937     csOp.opcode = '';
    938   } else {
    939     switch (csOp.opcode) {
    940     case '-':
    941       {
    942         if (csOp.chars <= attOp.chars) {
    943           // delete or delete part
    944           if (attOp.opcode == '=') {
    945             opOut.opcode = '-';
    946             opOut.chars = csOp.chars;
    947             opOut.lines = csOp.lines;
    948             opOut.attribs = '';
    949           }
    950           attOp.chars -= csOp.chars;
    951           attOp.lines -= csOp.lines;
    952           csOp.opcode = '';
    953           if (!attOp.chars) {
    954             attOp.opcode = '';
    955           }
    956         } else {
    957           // delete and keep going
    958           if (attOp.opcode == '=') {
    959             opOut.opcode = '-';
    960             opOut.chars = attOp.chars;
    961             opOut.lines = attOp.lines;
    962             opOut.attribs = '';
    963           }
    964           csOp.chars -= attOp.chars;
    965           csOp.lines -= attOp.lines;
    966           attOp.opcode = '';
    967         }
    968         break;
    969       }
    970     case '+':
    971       {
    972         // insert
    973         exports.copyOp(csOp, opOut);
    974         csOp.opcode = '';
    975         break;
    976       }
    977     case '=':
    978       {
    979         if (csOp.chars <= attOp.chars) {
    980           // keep or keep part
    981           opOut.opcode = attOp.opcode;
    982           opOut.chars = csOp.chars;
    983           opOut.lines = csOp.lines;
    984           opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
    985           csOp.opcode = '';
    986           attOp.chars -= csOp.chars;
    987           attOp.lines -= csOp.lines;
    988           if (!attOp.chars) {
    989             attOp.opcode = '';
    990           }
    991         } else {
    992           // keep and keep going
    993           opOut.opcode = attOp.opcode;
    994           opOut.chars = attOp.chars;
    995           opOut.lines = attOp.lines;
    996           opOut.attribs = exports.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool);
    997           attOp.opcode = '';
    998           csOp.chars -= attOp.chars;
    999           csOp.lines -= attOp.lines;
    1000         }
    1001         break;
    1002       }
    1003     case '':
    1004       {
    1005         exports.copyOp(attOp, opOut);
    1006         attOp.opcode = '';
    1007         break;
    1008       }
    1009     }
    1010   }
    1011 };
    1012
    1013 exports.applyToAttribution = function (cs, astr, pool) {
    1014   var unpacked = exports.unpack(cs);
    1015
    1016   return exports.applyZip(astr, 0, unpacked.ops, 0, function (op1, op2, opOut) {
    1017     return exports._slicerZipperFunc(op1, op2, opOut, pool);
    1018   });
    1019 };
    1020
    1021 /*exports.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) {
    1022   var iter = exports.opIterator(opsStr, optStartIndex);
    1023   var bankIndex = 0;
    1024
    1025 };*/
    1026
    1027 exports.mutateAttributionLines = function (cs, lines, pool) {
    1028   //dmesg(cs);
    1029   //dmesg(lines.toSource()+" ->");
    1030   var unpacked = exports.unpack(cs);
    1031   var csIter = exports.opIterator(unpacked.ops);
    1032   var csBank = unpacked.charBank;
    1033   var csBankIndex = 0;
    1034   // treat the attribution lines as text lines, mutating a line at a time
    1035   var mut = exports.textLinesMutator(lines);
    1036
    1037   var lineIter = null;
    1038
    1039   function isNextMutOp() {
    1040     return (lineIter && lineIter.hasNext()) || mut.hasMore();
    1041   }
    1042
    1043   function nextMutOp(destOp) {
    1044     if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) {
    1045       var line = mut.removeLines(1);
    1046       lineIter = exports.opIterator(line);
    1047     }
    1048     if (lineIter && lineIter.hasNext()) {
    1049       lineIter.next(destOp);
    1050     } else {
    1051       destOp.opcode = '';
    1052     }
    1053   }
    1054   var lineAssem = null;
    1055
    1056   function outputMutOp(op) {
    1057     //print("outputMutOp: "+op.toSource());
    1058     if (!lineAssem) {
    1059       lineAssem = exports.mergingOpAssembler();
    1060     }
    1061     lineAssem.append(op);
    1062     if (op.lines > 0) {
    1063       exports.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines");
    1064       // ship it to the mut
    1065       mut.insert(lineAssem.toString(), 1);
    1066       lineAssem = null;
    1067     }
    1068   }
    1069
    1070   var csOp = exports.newOp();
    1071   var attOp = exports.newOp();
    1072   var opOut = exports.newOp();
    1073   while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) {
    1074     if ((!csOp.opcode) && csIter.hasNext()) {
    1075       csIter.next(csOp);
    1076     }
    1077     //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource());
    1078     //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null));
    1079     //print("csOp: "+csOp.toSource());
    1080     if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) {
    1081       break; // done
    1082     } else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) {
    1083       // skip multiple lines; this is what makes small changes not order of the document size
    1084       mut.skipLines(csOp.lines);
    1085       //print("skipped: "+csOp.lines);
    1086       csOp.opcode = '';
    1087     } else if (csOp.opcode == '+') {
    1088       if (csOp.lines > 1) {
    1089         var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex;
    1090         exports.copyOp(csOp, opOut);
    1091         csOp.chars -= firstLineLen;
    1092         csOp.lines--;
    1093         opOut.lines = 1;
    1094         opOut.chars = firstLineLen;
    1095       } else {
    1096         exports.copyOp(csOp, opOut);
    1097         csOp.opcode = '';
    1098       }
    1099       outputMutOp(opOut);
    1100       csBankIndex += opOut.chars;
    1101       opOut.opcode = '';
    1102     } else {
    1103       if ((!attOp.opcode) && isNextMutOp()) {
    1104         nextMutOp(attOp);
    1105       }
    1106       //print("attOp: "+attOp.toSource());
    1107       exports._slicerZipperFunc(attOp, csOp, opOut, pool);
    1108       if (opOut.opcode) {
    1109         outputMutOp(opOut);
    1110         opOut.opcode = '';
    1111       }
    1112     }
    1113   }
    1114
    1115   exports.assert(!lineAssem, "line assembler not finished");
    1116   mut.close();
    1117
    1118   //dmesg("-> "+lines.toSource());
    1119 };
    1120
    1121 exports.joinAttributionLines = function (theAlines) {
    1122   var assem = exports.mergingOpAssembler();
    1123   for (var i = 0; i < theAlines.length; i++) {
    1124     var aline = theAlines[i];
    1125     var iter = exports.opIterator(aline);
    1126     while (iter.hasNext()) {
    1127       assem.append(iter.next());
    1128     }
    1129   }
    1130   return assem.toString();
    1131 };
    1132
    1133 exports.splitAttributionLines = function (attrOps, text) {
    1134   var iter = exports.opIterator(attrOps);
    1135   var assem = exports.mergingOpAssembler();
    1136   var lines = [];
    1137   var pos = 0;
    1138
    1139   function appendOp(op) {
    1140     assem.append(op);
    1141     if (op.lines > 0) {
    1142       lines.push(assem.toString());
    1143       assem.clear();
    1144     }
    1145     pos += op.chars;
    1146   }
    1147
    1148   while (iter.hasNext()) {
    1149     var op = iter.next();
    1150     var numChars = op.chars;
    1151     var numLines = op.lines;
    1152     while (numLines > 1) {
    1153       var newlineEnd = text.indexOf('\n', pos) + 1;
    1154       exports.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines");
    1155       op.chars = newlineEnd - pos;
    1156       op.lines = 1;
    1157       appendOp(op);
    1158       numChars -= op.chars;
    1159       numLines -= op.lines;
    1160     }
    1161     if (numLines == 1) {
    1162       op.chars = numChars;
    1163       op.lines = 1;
    1164     }
    1165     appendOp(op);
    1166   }
    1167
    1168   return lines;
    1169 };
    1170
    1171 exports.splitTextLines = function (text) {
    1172   return text.match(/[^\n]*(?:\n|[^\n]$)/g);
    1173 };
    1174
    1175 exports.compose = function (cs1, cs2, pool) {
    1176   var unpacked1 = exports.unpack(cs1);
    1177   var unpacked2 = exports.unpack(cs2);
    1178   var len1 = unpacked1.oldLen;
    1179   var len2 = unpacked1.newLen;
    1180   exports.assert(len2 == unpacked2.oldLen, "mismatched composition");
    1181   var len3 = unpacked2.newLen;
    1182   var bankIter1 = exports.stringIterator(unpacked1.charBank);
    1183   var bankIter2 = exports.stringIterator(unpacked2.charBank);
    1184   var bankAssem = exports.stringAssembler();
    1185
    1186   var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) {
    1187     //var debugBuilder = exports.stringAssembler();
    1188     //debugBuilder.append(exports.opString(op1));
    1189     //debugBuilder.append(',');
    1190     //debugBuilder.append(exports.opString(op2));
    1191     //debugBuilder.append(' / ');
    1192     var op1code = op1.opcode;
    1193     var op2code = op2.opcode;
    1194     if (op1code == '+' && op2code == '-') {
    1195       bankIter1.skip(Math.min(op1.chars, op2.chars));
    1196     }
    1197     exports._slicerZipperFunc(op1, op2, opOut, pool);
    1198     if (opOut.opcode == '+') {
    1199       if (op2code == '+') {
    1200         bankAssem.append(bankIter2.take(opOut.chars));
    1201       } else {
    1202         bankAssem.append(bankIter1.take(opOut.chars));
    1203       }
    1204     }
    1205
    1206     //debugBuilder.append(exports.opString(op1));
    1207     //debugBuilder.append(',');
    1208     //debugBuilder.append(exports.opString(op2));
    1209     //debugBuilder.append(' -> ');
    1210     //debugBuilder.append(exports.opString(opOut));
    1211     //print(debugBuilder.toString());
    1212   });
    1213
    1214   return exports.pack(len1, len3, newOps, bankAssem.toString());
    1215 };
    1216
    1217 exports.attributeTester = function (attribPair, pool) {
    1218   // returns a function that tests if a string of attributes
    1219   // (e.g. *3*4) contains a given attribute key,value that
    1220   // is already present in the pool.
    1221   if (!pool) {
    1222     return never;
    1223   }
    1224   var attribNum = pool.putAttrib(attribPair, true);
    1225   if (attribNum < 0) {
    1226     return never;
    1227   } else {
    1228     var re = new RegExp('\\*' + exports.numToString(attribNum) + '(?!\\w)');
    1229     return function (attribs) {
    1230       return re.test(attribs);
    1231     };
    1232   }
    1233
    1234   function never(attribs) {
    1235     return false;
    1236   }
    1237 };
    1238
    1239 exports.identity = function (N) {
    1240   return exports.pack(N, N, "", "");
    1241 };
    1242
    1243 exports.makeSplice = function (oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) {
    1244   var oldLen = oldFullText.length;
    1245
    1246   if (spliceStart >= oldLen) {
    1247     spliceStart = oldLen - 1;
    1248   }
    1249   if (numRemoved > oldFullText.length - spliceStart - 1) {
    1250     numRemoved = oldFullText.length - spliceStart - 1;
    1251   }
    1252   var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved);
    1253   var newLen = oldLen + newText.length - oldText.length;
    1254
    1255   var assem = exports.smartOpAssembler();
    1256   assem.appendOpWithText('=', oldFullText.substring(0, spliceStart));
    1257   assem.appendOpWithText('-', oldText);
    1258   assem.appendOpWithText('+', newText, optNewTextAPairs, pool);
    1259   assem.endDocument();
    1260   return exports.pack(oldLen, newLen, assem.toString(), newText);
    1261 };
    1262
    1263 exports.toSplices = function (cs) {
    1264   // get a list of splices, [startChar, endChar, newText]
    1265   var unpacked = exports.unpack(cs);
    1266   var splices = [];
    1267
    1268   var oldPos = 0;
    1269   var iter = exports.opIterator(unpacked.ops);
    1270   var charIter = exports.stringIterator(unpacked.charBank);
    1271   var inSplice = false;
    1272   while (iter.hasNext()) {
    1273     var op = iter.next();
    1274     if (op.opcode == '=') {
    1275       oldPos += op.chars;
    1276       inSplice = false;
    1277     } else {
    1278       if (!inSplice) {
    1279         splices.push([oldPos, oldPos, ""]);
    1280         inSplice = true;
    1281       }
    1282       if (op.opcode == '-') {
    1283         oldPos += op.chars;
    1284         splices[splices.length - 1][1] += op.chars;
    1285       } else if (op.opcode == '+') {
    1286         splices[splices.length - 1][2] += charIter.take(op.chars);
    1287       }
    1288     }
    1289   }
    1290
    1291   return splices;
    1292 };
    1293
    1294 exports.characterRangeFollow = function (cs, startChar, endChar, insertionsAfter) {
    1295   var newStartChar = startChar;
    1296   var newEndChar = endChar;
    1297   var splices = exports.toSplices(cs);
    1298   var lengthChangeSoFar = 0;
    1299   for (var i = 0; i < splices.length; i++) {
    1300     var splice = splices[i];
    1301     var spliceStart = splice[0] + lengthChangeSoFar;
    1302     var spliceEnd = splice[1] + lengthChangeSoFar;
    1303     var newTextLength = splice[2].length;
    1304     var thisLengthChange = newTextLength - (spliceEnd - spliceStart);
    1305
    1306     if (spliceStart <= newStartChar && spliceEnd >= newEndChar) {
    1307       // splice fully replaces/deletes range
    1308       // (also case that handles insertion at a collapsed selection)
    1309       if (insertionsAfter) {
    1310         newStartChar = newEndChar = spliceStart;
    1311       } else {
    1312         newStartChar = newEndChar = spliceStart + newTextLength;
    1313       }
    1314     } else if (spliceEnd <= newStartChar) {
    1315       // splice is before range
    1316       newStartChar += thisLengthChange;
    1317       newEndChar += thisLengthChange;
    1318     } else if (spliceStart >= newEndChar) {
    1319       // splice is after range
    1320     } else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) {
    1321       // splice is inside range
    1322       newEndChar += thisLengthChange;
    1323     } else if (spliceEnd < newEndChar) {
    1324       // splice overlaps beginning of range
    1325       newStartChar = spliceStart + newTextLength;
    1326       newEndChar += thisLengthChange;
    1327     } else {
    1328       // splice overlaps end of range
    1329       newEndChar = spliceStart;
    1330     }
    1331
    1332     lengthChangeSoFar += thisLengthChange;
    1333   }
    1334
    1335   return [newStartChar, newEndChar];
    1336 };
    1337
    1338 exports.moveOpsToNewPool = function (cs, oldPool, newPool) {
    1339   // works on exports or attribution string
    1340   var dollarPos = cs.indexOf('$');
    1341   if (dollarPos < 0) {
    1342     dollarPos = cs.length;
    1343   }
    1344   var upToDollar = cs.substring(0, dollarPos);
    1345   var fromDollar = cs.substring(dollarPos);
    1346   // order of attribs stays the same
    1347   return upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) {
    1348     var oldNum = exports.parseNum(a);
    1349     var pair = oldPool.getAttrib(oldNum);
    1350     var newNum = newPool.putAttrib(pair);
    1351     return '*' + exports.numToString(newNum);
    1352   }) + fromDollar;
    1353 };
    1354
    1355 exports.makeAttribution = function (text) {
    1356   var assem = exports.smartOpAssembler();
    1357   assem.appendOpWithText('+', text);
    1358   return assem.toString();
    1359 };
    1360
    1361 // callable on a exports, attribution string, or attribs property of an op
    1362 exports.eachAttribNumber = function (cs, func) {
    1363   var dollarPos = cs.indexOf('$');
    1364   if (dollarPos < 0) {
    1365     dollarPos = cs.length;
    1366   }
    1367   var upToDollar = cs.substring(0, dollarPos);
    1368
    1369   upToDollar.replace(/\*([0-9a-z]+)/g, function (_, a) {
    1370     func(exports.parseNum(a));
    1371     return '';
    1372   });
    1373 };
    1374
    1375 // callable on a exports, attribution string, or attribs property of an op,
    1376 // though it may easily create adjacent ops that can be merged.
    1377 exports.filterAttribNumbers = function (cs, filter) {
    1378   return exports.mapAttribNumbers(cs, filter);
    1379 };
    1380
    1381 exports.mapAttribNumbers = function (cs, func) {
    1382   var dollarPos = cs.indexOf('$');
    1383   if (dollarPos < 0) {
    1384     dollarPos = cs.length;
    1385   }
    1386   var upToDollar = cs.substring(0, dollarPos);
    1387
    1388   var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function (s, a) {
    1389     var n = func(exports.parseNum(a));
    1390     if (n === true) {
    1391       return s;
    1392     } else if ((typeof n) === "number") {
    1393       return '*' + exports.numToString(n);
    1394     } else {
    1395       return '';
    1396     }
    1397   });
    1398
    1399   return newUpToDollar + cs.substring(dollarPos);
    1400 };
    1401
    1402 exports.makeAText = function (text, attribs) {
    1403   return {
    1404     text: text,
    1405     attribs: (attribs || exports.makeAttribution(text))
    1406   };
    1407 };
    1408
    1409 exports.applyToAText = function (cs, atext, pool) {
    1410   return {
    1411     text: exports.applyToText(cs, atext.text),
    1412     attribs: exports.applyToAttribution(cs, atext.attribs, pool)
    1413   };
    1414 };
    1415
    1416 exports.cloneAText = function (atext) {
    1417   return {
    1418     text: atext.text,
    1419     attribs: atext.attribs
    1420   };
    1421 };
    1422
    1423 exports.copyAText = function (atext1, atext2) {
    1424   atext2.text = atext1.text;
    1425   atext2.attribs = atext1.attribs;
    1426 };
    1427
    1428 exports.appendATextToAssembler = function (atext, assem) {
    1429   // intentionally skips last newline char of atext
    1430   var iter = exports.opIterator(atext.attribs);
    1431   var op = exports.newOp();
    1432   while (iter.hasNext()) {
    1433     iter.next(op);
    1434     if (!iter.hasNext()) {
    1435       // last op, exclude final newline
    1436       if (op.lines <= 1) {
    1437         op.lines = 0;
    1438         op.chars--;
    1439         if (op.chars) {
    1440           assem.append(op);
    1441         }
    1442       } else {
    1443         var nextToLastNewlineEnd =
    1444         atext.text.lastIndexOf('\n', atext.text.length - 2) + 1;
    1445         var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1;
    1446         op.lines--;
    1447         op.chars -= (lastLineLength + 1);
    1448         assem.append(op);
    1449         op.lines = 0;
    1450         op.chars = lastLineLength;
    1451         if (op.chars) {
    1452           assem.append(op);
    1453         }
    1454       }
    1455     } else {
    1456       assem.append(op);
    1457     }
    1458   }
    1459 };
    1460
    1461 exports.prepareForWire = function (cs, pool) {
    1462   var newPool = AttributePoolFactory.createAttributePool();;
    1463   var newCs = exports.moveOpsToNewPool(cs, pool, newPool);
    1464   return {
    1465     translated: newCs,
    1466     pool: newPool
    1467   };
    1468 };
    1469
    1470 exports.isIdentity = function (cs) {
    1471   var unpacked = exports.unpack(cs);
    1472   return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen;
    1473 };
    1474
    1475 exports.opAttributeValue = function (op, key, pool) {
    1476   return exports.attribsAttributeValue(op.attribs, key, pool);
    1477 };
    1478
    1479 exports.attribsAttributeValue = function (attribs, key, pool) {
    1480   var value = '';
    1481   if (attribs) {
    1482     exports.eachAttribNumber(attribs, function (n) {
    1483       if (pool.getAttribKey(n) == key) {
    1484         value = pool.getAttribValue(n);
    1485       }
    1486     });
    1487   }
    1488   return value;
    1489 };
    1490
    1491 exports.builder = function (oldLen) {
    1492   var assem = exports.smartOpAssembler();
    1493   var o = exports.newOp();
    1494   var charBank = exports.stringAssembler();
    1495
    1496   var self = {
    1497     // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case)
    1498     keep: function (N, L, attribs, pool) {
    1499       o.opcode = '=';
    1500       o.attribs = (attribs && exports.makeAttribsString('=', attribs, pool)) || '';
    1501       o.chars = N;
    1502       o.lines = (L || 0);
    1503       assem.append(o);
    1504       return self;
    1505     },
    1506     keepText: function (text, attribs, pool) {
    1507       assem.appendOpWithText('=', text, attribs, pool);
    1508       return self;
    1509     },
    1510     insert: function (text, attribs, pool) {
    1511       assem.appendOpWithText('+', text, attribs, pool);
    1512       charBank.append(text);
    1513       return self;
    1514     },
    1515     remove: function (N, L) {
    1516       o.opcode = '-';
    1517       o.attribs = '';
    1518       o.chars = N;
    1519       o.lines = (L || 0);
    1520       assem.append(o);
    1521       return self;
    1522     },
    1523     toString: function () {
    1524       assem.endDocument();
    1525       var newLen = oldLen + assem.getLengthChange();
    1526       return exports.pack(oldLen, newLen, assem.toString(), charBank.toString());
    1527     }
    1528   };
    1529
    1530   return self;
    1531 };
    1532
    1533 exports.makeAttribsString = function (opcode, attribs, pool) {
    1534   // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work
    1535   if (!attribs) {
    1536     return '';
    1537   } else if ((typeof attribs) == "string") {
    1538     return attribs;
    1539   } else if (pool && attribs && attribs.length) {
    1540     if (attribs.length > 1) {
    1541       attribs = attribs.slice();
    1542       attribs.sort();
    1543     }
    1544     var result = [];
    1545     for (var i = 0; i < attribs.length; i++) {
    1546       var pair = attribs[i];
    1547       if (opcode == '=' || (opcode == '+' && pair[1])) {
    1548         result.push('*' + exports.numToString(pool.putAttrib(pair)));
    1549       }
    1550     }
    1551     return result.join('');
    1552   }
    1553 };
    1554
    1555 // like "substring" but on a single-line attribution string
    1556 exports.subattribution = function (astr, start, optEnd) {
    1557   var iter = exports.opIterator(astr, 0);
    1558   var assem = exports.smartOpAssembler();
    1559   var attOp = exports.newOp();
    1560   var csOp = exports.newOp();
    1561   var opOut = exports.newOp();
    1562
    1563   function doCsOp() {
    1564     if (csOp.chars) {
    1565       while (csOp.opcode && (attOp.opcode || iter.hasNext())) {
    1566         if (!attOp.opcode) iter.next(attOp);
    1567
    1568         if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) {
    1569           csOp.lines++;
    1570         }
    1571
    1572         exports._slicerZipperFunc(attOp, csOp, opOut, null);
    1573         if (opOut.opcode) {
    1574           assem.append(opOut);
    1575           opOut.opcode = '';
    1576         }
    1577       }
    1578     }
    1579   }
    1580
    1581   csOp.opcode = '-';
    1582   csOp.chars = start;
    1583
    1584   doCsOp();
    1585
    1586   if (optEnd === undefined) {
    1587     if (attOp.opcode) {
    1588       assem.append(attOp);
    1589     }
    1590     while (iter.hasNext()) {
    1591       iter.next(attOp);
    1592       assem.append(attOp);
    1593     }
    1594   } else {
    1595     csOp.opcode = '=';
    1596     csOp.chars = optEnd - start;
    1597     doCsOp();
    1598   }
    1599
    1600   return assem.toString();
    1601 };
    1602
    1603 exports.inverse = function (cs, lines, alines, pool) {
    1604   // lines and alines are what the exports is meant to apply to.
    1605   // They may be arrays or objects with .get(i) and .length methods.
    1606   // They include final newlines on lines.
    1607
    1608   function lines_get(idx) {
    1609     if (lines.get) {
    1610       return lines.get(idx);
    1611     } else {
    1612       return lines[idx];
    1613     }
    1614   }
    1615
    1616   function lines_length() {
    1617     if ((typeof lines.length) == "number") {
    1618       return lines.length;
    1619     } else {
    1620       return lines.length();
    1621     }
    1622   }
    1623
    1624   function alines_get(idx) {
    1625     if (alines.get) {
    1626       return alines.get(idx);
    1627     } else {
    1628       return alines[idx];
    1629     }
    1630   }
    1631
    1632   function alines_length() {
    1633     if ((typeof alines.length) == "number") {
    1634       return alines.length;
    1635     } else {
    1636       return alines.length();
    1637     }
    1638   }
    1639
    1640   var curLine = 0;
    1641   var curChar = 0;
    1642   var curLineOpIter = null;
    1643   var curLineOpIterLine;
    1644   var curLineNextOp = exports.newOp('+');
    1645
    1646   var unpacked = exports.unpack(cs);
    1647   var csIter = exports.opIterator(unpacked.ops);
    1648   var builder = exports.builder(unpacked.newLen);
    1649
    1650   function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) {
    1651
    1652     if ((!curLineOpIter) || (curLineOpIterLine != curLine)) {
    1653       // create curLineOpIter and advance it to curChar
    1654       curLineOpIter = exports.opIterator(alines_get(curLine));
    1655       curLineOpIterLine = curLine;
    1656       var indexIntoLine = 0;
    1657       var done = false;
    1658       while (!done) {
    1659         curLineOpIter.next(curLineNextOp);
    1660         if (indexIntoLine + curLineNextOp.chars >= curChar) {
    1661           curLineNextOp.chars -= (curChar - indexIntoLine);
    1662           done = true;
    1663         } else {
    1664           indexIntoLine += curLineNextOp.chars;
    1665         }
    1666       }
    1667     }
    1668
    1669     while (numChars > 0) {
    1670       if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
    1671         curLine++;
    1672         curChar = 0;
    1673         curLineOpIterLine = curLine;
    1674         curLineNextOp.chars = 0;
    1675         curLineOpIter = exports.opIterator(alines_get(curLine));
    1676       }
    1677       if (!curLineNextOp.chars) {
    1678         curLineOpIter.next(curLineNextOp);
    1679       }
    1680       var charsToUse = Math.min(numChars, curLineNextOp.chars);
    1681       func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0);
    1682       numChars -= charsToUse;
    1683       curLineNextOp.chars -= charsToUse;
    1684       curChar += charsToUse;
    1685     }
    1686
    1687     if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) {
    1688       curLine++;
    1689       curChar = 0;
    1690     }
    1691   }
    1692
    1693   function skip(N, L) {
    1694     if (L) {
    1695       curLine += L;
    1696       curChar = 0;
    1697     } else {
    1698       if (curLineOpIter && curLineOpIterLine == curLine) {
    1699         consumeAttribRuns(N, function () {});
    1700       } else {
    1701         curChar += N;
    1702       }
    1703     }
    1704   }
    1705
    1706   function nextText(numChars) {
    1707     var len = 0;
    1708     var assem = exports.stringAssembler();
    1709     var firstString = lines_get(curLine).substring(curChar);
    1710     len += firstString.length;
    1711     assem.append(firstString);
    1712
    1713     var lineNum = curLine + 1;
    1714     while (len < numChars) {
    1715       var nextString = lines_get(lineNum);
    1716       len += nextString.length;
    1717       assem.append(nextString);
    1718       lineNum++;
    1719     }
    1720
    1721     return assem.toString().substring(0, numChars);
    1722   }
    1723
    1724   function cachedStrFunc(func) {
    1725     var cache = {};
    1726     return function (s) {
    1727       if (!cache[s]) {
    1728         cache[s] = func(s);
    1729       }
    1730       return cache[s];
    1731     };
    1732   }
    1733
    1734   var attribKeys = [];
    1735   var attribValues = [];
    1736   while (csIter.hasNext()) {
    1737     var csOp = csIter.next();
    1738     if (csOp.opcode == '=') {
    1739       if (csOp.attribs) {
    1740         attribKeys.length = 0;
    1741         attribValues.length = 0;
    1742         exports.eachAttribNumber(csOp.attribs, function (n) {
    1743           attribKeys.push(pool.getAttribKey(n));
    1744           attribValues.push(pool.getAttribValue(n));
    1745         });
    1746         var undoBackToAttribs = cachedStrFunc(function (attribs) {
    1747           var backAttribs = [];
    1748           for (var i = 0; i < attribKeys.length; i++) {
    1749             var appliedKey = attribKeys[i];
    1750             var appliedValue = attribValues[i];
    1751             var oldValue = exports.attribsAttributeValue(attribs, appliedKey, pool);
    1752             if (appliedValue != oldValue) {
    1753               backAttribs.push([appliedKey, oldValue]);
    1754             }
    1755           }
    1756           return exports.makeAttribsString('=', backAttribs, pool);
    1757         });
    1758         consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) {
    1759           builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs));
    1760         });
    1761       } else {
    1762         skip(csOp.chars, csOp.lines);
    1763         builder.keep(csOp.chars, csOp.lines);
    1764       }
    1765     } else if (csOp.opcode == '+') {
    1766       builder.remove(csOp.chars, csOp.lines);
    1767     } else if (csOp.opcode == '-') {
    1768       var textBank = nextText(csOp.chars);
    1769       var textBankIndex = 0;
    1770       consumeAttribRuns(csOp.chars, function (len, attribs, endsLine) {
    1771         builder.insert(textBank.substr(textBankIndex, len), attribs);
    1772         textBankIndex += len;
    1773       });
    1774     }
    1775   }
    1776
    1777   return exports.checkRep(builder.toString());
    1778 };
    1779
    1780 // %CLIENT FILE ENDS HERE%
    1781 exports.follow = function (cs1, cs2, reverseInsertOrder, pool) {
    1782   var unpacked1 = exports.unpack(cs1);
    1783   var unpacked2 = exports.unpack(cs2);
    1784   var len1 = unpacked1.oldLen;
    1785   var len2 = unpacked2.oldLen;
    1786   exports.assert(len1 == len2, "mismatched follow");
    1787   var chars1 = exports.stringIterator(unpacked1.charBank);
    1788   var chars2 = exports.stringIterator(unpacked2.charBank);
    1789
    1790   var oldLen = unpacked1.newLen;
    1791   var oldPos = 0;
    1792   var newLen = 0;
    1793
    1794   var hasInsertFirst = exports.attributeTester(['insertorder', 'first'], pool);
    1795
    1796   var newOps = exports.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function (op1, op2, opOut) {
    1797     if (op1.opcode == '+' || op2.opcode == '+') {
    1798       var whichToDo;
    1799       if (op2.opcode != '+') {
    1800         whichToDo = 1;
    1801       } else if (op1.opcode != '+') {
    1802         whichToDo = 2;
    1803       } else {
    1804         // both +
    1805         var firstChar1 = chars1.peek(1);
    1806         var firstChar2 = chars2.peek(1);
    1807         var insertFirst1 = hasInsertFirst(op1.attribs);
    1808         var insertFirst2 = hasInsertFirst(op2.attribs);
    1809         if (insertFirst1 && !insertFirst2) {
    1810           whichToDo = 1;
    1811         } else if (insertFirst2 && !insertFirst1) {
    1812           whichToDo = 2;
    1813         }
    1814         // insert string that doesn't start with a newline first so as not to break up lines
    1815         else if (firstChar1 == '\n' && firstChar2 != '\n') {
    1816           whichToDo = 2;
    1817         } else if (firstChar1 != '\n' && firstChar2 == '\n') {
    1818           whichToDo = 1;
    1819         }
    1820         // break symmetry:
    1821         else if (reverseInsertOrder) {
    1822           whichToDo = 2;
    1823         } else {
    1824           whichToDo = 1;
    1825         }
    1826       }
    1827       if (whichToDo == 1) {
    1828         chars1.skip(op1.chars);
    1829         opOut.opcode = '=';
    1830         opOut.lines = op1.lines;
    1831         opOut.chars = op1.chars;
    1832         opOut.attribs = '';
    1833         op1.opcode = '';
    1834       } else {
    1835         // whichToDo == 2
    1836         chars2.skip(op2.chars);
    1837         exports.copyOp(op2, opOut);
    1838         op2.opcode = '';
    1839       }
    1840     } else if (op1.opcode == '-') {
    1841       if (!op2.opcode) {
    1842         op1.opcode = '';
    1843       } else {
    1844         if (op1.chars <= op2.chars) {
    1845           op2.chars -= op1.chars;
    1846           op2.lines -= op1.lines;
    1847           op1.opcode = '';
    1848           if (!op2.chars) {
    1849             op2.opcode = '';
    1850           }
    1851         } else {
    1852           op1.chars -= op2.chars;
    1853           op1.lines -= op2.lines;
    1854           op2.opcode = '';
    1855         }
    1856       }
    1857     } else if (op2.opcode == '-') {
    1858       exports.copyOp(op2, opOut);
    1859       if (!op1.opcode) {
    1860         op2.opcode = '';
    1861       } else if (op2.chars <= op1.chars) {
    1862         // delete part or all of a keep
    1863         op1.chars -= op2.chars;
    1864         op1.lines -= op2.lines;
    1865         op2.opcode = '';
    1866         if (!op1.chars) {
    1867           op1.opcode = '';
    1868         }
    1869       } else {
    1870         // delete all of a keep, and keep going
    1871         opOut.lines = op1.lines;
    1872         opOut.chars = op1.chars;
    1873         op2.lines -= op1.lines;
    1874         op2.chars -= op1.chars;
    1875         op1.opcode = '';
    1876       }
    1877     } else if (!op1.opcode) {
    1878       exports.copyOp(op2, opOut);
    1879       op2.opcode = '';
    1880     } else if (!op2.opcode) {
    1881       exports.copyOp(op1, opOut);
    1882       op1.opcode = '';
    1883     } else {
    1884       // both keeps
    1885       opOut.opcode = '=';
    1886       opOut.attribs = exports.followAttributes(op1.attribs, op2.attribs, pool);
    1887       if (op1.chars <= op2.chars) {
    1888         opOut.chars = op1.chars;
    1889         opOut.lines = op1.lines;
    1890         op2.chars -= op1.chars;
    1891         op2.lines -= op1.lines;
    1892         op1.opcode = '';
    1893         if (!op2.chars) {
    1894           op2.opcode = '';
    1895         }
    1896       } else {
    1897         opOut.chars = op2.chars;
    1898         opOut.lines = op2.lines;
    1899         op1.chars -= op2.chars;
    1900         op1.lines -= op2.lines;
    1901         op2.opcode = '';
    1902       }
    1903     }
    1904     switch (opOut.opcode) {
    1905     case '=':
    1906       oldPos += opOut.chars;
    1907       newLen += opOut.chars;
    1908       break;
    1909     case '-':
    1910       oldPos += opOut.chars;
    1911       break;
    1912     case '+':
    1913       newLen += opOut.chars;
    1914       break;
    1915     }
    1916   });
    1917   newLen += oldLen - oldPos;
    1918
    1919   return exports.pack(oldLen, newLen, newOps, unpacked2.charBank);
    1920 };
    1921
    1922 exports.followAttributes = function (att1, att2, pool) {
    1923   // The merge of two sets of attribute changes to the same text
    1924   // takes the lexically-earlier value if there are two values
    1925   // for the same key.  Otherwise, all key/value changes from
    1926   // both attribute sets are taken.  This operation is the "follow",
    1927   // so a set of changes is produced that can be applied to att1
    1928   // to produce the merged set.
    1929   if ((!att2) || (!pool)) return '';
    1930   if (!att1) return att2;
    1931   var atts = [];
    1932   att2.replace(/\*([0-9a-z]+)/g, function (_, a) {
    1933     atts.push(pool.getAttrib(exports.parseNum(a)));
    1934     return '';
    1935   });
    1936   att1.replace(/\*([0-9a-z]+)/g, function (_, a) {
    1937     var pair1 = pool.getAttrib(exports.parseNum(a));
    1938     for (var i = 0; i < atts.length; i++) {
    1939       var pair2 = atts[i];
    1940       if (pair1[0] == pair2[0]) {
    1941         if (pair1[1] <= pair2[1]) {
    1942           // winner of merge is pair1, delete this attribute
    1943           atts.splice(i, 1);
    1944         }
    1945         break;
    1946       }
    1947     }
    1948     return '';
    1949   });
    1950   // we've only removed attributes, so they're already sorted
    1951   var buf = exports.stringAssembler();
    1952   for (var i = 0; i < atts.length; i++) {
    1953     buf.append('*');
    1954     buf.append(exports.numToString(pool.putAttrib(atts[i])));
    1955   }
    1956   return buf.toString();
    1957 };
    1958