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