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