diff --git a/bin/checkPad.js b/bin/checkPad.js index 9e054441..356b0779 100644 --- a/bin/checkPad.js +++ b/bin/checkPad.js @@ -15,7 +15,8 @@ var log4js = require("log4js"); log4js.setGlobalLogLevel("INFO"); var async = require("async"); var db = require('../node/db/DB'); -var Changeset = require('../node/utils/Changeset'); +var CommonCode = require('../node/utils/common_code'); +var Changeset = CommonCode.require("/Changeset"); var padManager; async.series([ diff --git a/bin/convert.js b/bin/convert.js index 4302114c..f8ea77e8 100644 --- a/bin/convert.js +++ b/bin/convert.js @@ -1,11 +1,12 @@ +var CommonCode = require('../node/utils/common_code'); var startTime = new Date().getTime(); var fs = require("fs"); var ueberDB = require("ueberDB"); var mysql = require("mysql"); var async = require("async"); -var Changeset = require("../node/utils/Changeset"); +var Changeset = CommonCode.require("/Changeset"); var randomString = require("../node/utils/randomstring"); -var AttributePoolFactory = require("../node/utils/AttributePoolFactory"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var settingsFile = process.argv[2]; var sqlOutputFile = process.argv[3]; diff --git a/node/db/Pad.js b/node/db/Pad.js index 632eebe8..f29f7173 100644 --- a/node/db/Pad.js +++ b/node/db/Pad.js @@ -2,9 +2,10 @@ * The pad object, defined with joose */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); -var Changeset = require("../utils/Changeset"); -var AttributePoolFactory = require("../utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var db = require("./DB").db; var async = require("async"); var settings = require('../utils/Settings'); diff --git a/node/easysync_tests.js b/node/easysync_tests.js index 5b73b717..8e7398be 100644 --- a/node/easysync_tests.js +++ b/node/easysync_tests.js @@ -20,8 +20,9 @@ * limitations under the License. */ -var Changeset = require('./utils/Changeset'); -var AttributePoolFactory = require("./utils/AttributePoolFactory"); +var CommonCode = require('./utils/common_code'); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); function random() { this.nextInt = function (maxValue) { diff --git a/node/handler/PadMessageHandler.js b/node/handler/PadMessageHandler.js index 4a078542..19aa1f9b 100644 --- a/node/handler/PadMessageHandler.js +++ b/node/handler/PadMessageHandler.js @@ -18,11 +18,12 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var async = require("async"); var padManager = require("../db/PadManager"); -var Changeset = require("../utils/Changeset"); -var AttributePoolFactory = require("../utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var authorManager = require("../db/AuthorManager"); var readOnlyManager = require("../db/ReadOnlyManager"); var settings = require('../utils/Settings'); diff --git a/node/handler/TimesliderMessageHandler.js b/node/handler/TimesliderMessageHandler.js index b30a9fc9..18806843 100644 --- a/node/handler/TimesliderMessageHandler.js +++ b/node/handler/TimesliderMessageHandler.js @@ -18,11 +18,12 @@ * limitations under the License. */ +var CommonCode = require('../utils/common_code'); var ERR = require("async-stacktrace"); var async = require("async"); var padManager = require("../db/PadManager"); -var Changeset = require("../utils/Changeset"); -var AttributePoolFactory = require("../utils/AttributePoolFactory"); +var Changeset = CommonCode.require("/Changeset"); +var AttributePoolFactory = CommonCode.require("/AttributePoolFactory"); var settings = require('../utils/Settings'); var authorManager = require("../db/AuthorManager"); var log4js = require('log4js'); diff --git a/node/utils/ExportDokuWiki.js b/node/utils/ExportDokuWiki.js index 48e4b291..abe6d347 100644 --- a/node/utils/ExportDokuWiki.js +++ b/node/utils/ExportDokuWiki.js @@ -15,7 +15,8 @@ */ var async = require("async"); -var Changeset = require("./Changeset"); +var CommonCode = require('./common_code'); +var Changeset = CommonCode.require("/Changeset"); var padManager = require("../db/PadManager"); function getPadDokuWiki(pad, revNum, callback) diff --git a/node/utils/ExportHtml.js b/node/utils/ExportHtml.js index d4be80d2..7296012f 100644 --- a/node/utils/ExportHtml.js +++ b/node/utils/ExportHtml.js @@ -14,8 +14,9 @@ * limitations under the License. */ +var CommonCode = require('./common_code'); var async = require("async"); -var Changeset = require("./Changeset"); +var Changeset = CommonCode.require("/Changeset"); var padManager = require("../db/PadManager"); var ERR = require("async-stacktrace"); diff --git a/node/utils/ImportHtml.js b/node/utils/ImportHtml.js index 1b0bcaea..ce866369 100644 --- a/node/utils/ImportHtml.js +++ b/node/utils/ImportHtml.js @@ -17,9 +17,10 @@ var jsdom = require('jsdom-nocontextifiy').jsdom; var log4js = require('log4js'); -var Changeset = require("./Changeset"); -var contentcollector = require("./contentcollector"); -var map = require("../../static/js/ace2_common.js").map; +var CommonCode = require('../utils/common_code'); +var Changeset = CommonCode.require("/Changeset"); +var contentcollector = CommonCode.require("/contentcollector"); +var map = CommonCode.require("/ace2_common").map; function setPadHTML(pad, html, callback) { diff --git a/node/utils/contentcollector.js b/node/utils/contentcollector.js deleted file mode 100644 index a7fa940a..00000000 --- a/node/utils/contentcollector.js +++ /dev/null @@ -1,692 +0,0 @@ -// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector -// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset"); -// %APPJET%: import("etherpad.admin.plugins"); -/** - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS-IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -var Changeset = require("../utils/Changeset"); - -var _MAX_LIST_LEVEL = 8; - -function sanitizeUnicode(s) -{ - return s.replace(/[\uffff\ufffe\ufeff\ufdd0-\ufdef\ud800-\udfff]/g, '?'); -} - -function makeContentCollector(collectStyles, browser, apool, domInterface, className2Author) -{ - browser = browser || {}; - - var plugins_; - if (typeof(plugins) != 'undefined') - { - plugins_ = plugins; - } - else - { - plugins_ = {callHook: function () {}}; - } - - var dom = domInterface || { - isNodeText: function(n) - { - return (n.nodeType == 3); - }, - nodeTagName: function(n) - { - return n.tagName; - }, - nodeValue: function(n) - { - return n.nodeValue; - }, - nodeNumChildren: function(n) - { - return n.childNodes.length; - }, - nodeChild: function(n, i) - { - return n.childNodes.item(i); - }, - nodeProp: function(n, p) - { - return n[p]; - }, - nodeAttr: function(n, a) - { - return n.getAttribute(a); - }, - optNodeInnerHTML: function(n) - { - return n.innerHTML; - } - }; - - var _blockElems = { - "div": 1, - "p": 1, - "pre": 1, - "li": 1 - }; - - function isBlockElement(n) - { - return !!_blockElems[(dom.nodeTagName(n) || "").toLowerCase()]; - } - - function textify(str) - { - return sanitizeUnicode( - str.replace(/[\n\r ]/g, ' ').replace(/\xa0/g, ' ').replace(/\t/g, ' ')); - } - - function getAssoc(node, name) - { - return dom.nodeProp(node, "_magicdom_" + name); - } - - var lines = (function() - { - var textArray = []; - var attribsArray = []; - var attribsBuilder = null; - var op = Changeset.newOp('+'); - var self = { - length: function() - { - return textArray.length; - }, - atColumnZero: function() - { - return textArray[textArray.length - 1] === ""; - }, - startNew: function() - { - textArray.push(""); - self.flush(true); - attribsBuilder = Changeset.smartOpAssembler(); - }, - textOfLine: function(i) - { - return textArray[i]; - }, - appendText: function(txt, attrString) - { - textArray[textArray.length - 1] += txt; - //dmesg(txt+" / "+attrString); - op.attribs = attrString; - op.chars = txt.length; - attribsBuilder.append(op); - }, - textLines: function() - { - return textArray.slice(); - }, - attribLines: function() - { - return attribsArray; - }, - // call flush only when you're done - flush: function(withNewline) - { - if (attribsBuilder) - { - attribsArray.push(attribsBuilder.toString()); - attribsBuilder = null; - } - } - }; - self.startNew(); - return self; - }()); - var cc = {}; - - function _ensureColumnZero(state) - { - if (!lines.atColumnZero()) - { - cc.startNewLine(state); - } - } - var selection, startPoint, endPoint; - var selStart = [-1, -1], - selEnd = [-1, -1]; - var blockElems = { - "div": 1, - "p": 1, - "pre": 1 - }; - - function _isEmpty(node, state) - { - // consider clean blank lines pasted in IE to be empty - if (dom.nodeNumChildren(node) == 0) return true; - if (dom.nodeNumChildren(node) == 1 && getAssoc(node, "shouldBeEmpty") && dom.optNodeInnerHTML(node) == " " && !getAssoc(node, "unpasted")) - { - if (state) - { - var child = dom.nodeChild(node, 0); - _reachPoint(child, 0, state); - _reachPoint(child, 1, state); - } - return true; - } - return false; - } - - function _pointHere(charsAfter, state) - { - var ln = lines.length() - 1; - var chr = lines.textOfLine(ln).length; - if (chr == 0 && state.listType && state.listType != 'none') - { - chr += 1; // listMarker - } - chr += charsAfter; - return [ln, chr]; - } - - function _reachBlockPoint(nd, idx, state) - { - if (!dom.isNodeText(nd)) _reachPoint(nd, idx, state); - } - - function _reachPoint(nd, idx, state) - { - if (startPoint && nd == startPoint.node && startPoint.index == idx) - { - selStart = _pointHere(0, state); - } - if (endPoint && nd == endPoint.node && endPoint.index == idx) - { - selEnd = _pointHere(0, state); - } - } - cc.incrementFlag = function(state, flagName) - { - state.flags[flagName] = (state.flags[flagName] || 0) + 1; - } - cc.decrementFlag = function(state, flagName) - { - state.flags[flagName]--; - } - cc.incrementAttrib = function(state, attribName) - { - if (!state.attribs[attribName]) - { - state.attribs[attribName] = 1; - } - else - { - state.attribs[attribName]++; - } - _recalcAttribString(state); - } - cc.decrementAttrib = function(state, attribName) - { - state.attribs[attribName]--; - _recalcAttribString(state); - } - - function _enterList(state, listType) - { - var oldListType = state.listType; - state.listLevel = (state.listLevel || 0) + 1; - if (listType != 'none') - { - state.listNesting = (state.listNesting || 0) + 1; - } - state.listType = listType; - _recalcAttribString(state); - return oldListType; - } - - function _exitList(state, oldListType) - { - state.listLevel--; - if (state.listType != 'none') - { - state.listNesting--; - } - state.listType = oldListType; - _recalcAttribString(state); - } - - function _enterAuthor(state, author) - { - var oldAuthor = state.author; - state.authorLevel = (state.authorLevel || 0) + 1; - state.author = author; - _recalcAttribString(state); - return oldAuthor; - } - - function _exitAuthor(state, oldAuthor) - { - state.authorLevel--; - state.author = oldAuthor; - _recalcAttribString(state); - } - - function _recalcAttribString(state) - { - var lst = []; - for (var a in state.attribs) - { - if (state.attribs[a]) - { - lst.push([a, 'true']); - } - } - if (state.authorLevel > 0) - { - var authorAttrib = ['author', state.author]; - if (apool.putAttrib(authorAttrib, true) >= 0) - { - // require that author already be in pool - // (don't add authors from other documents, etc.) - lst.push(authorAttrib); - } - } - state.attribString = Changeset.makeAttribsString('+', lst, apool); - } - - function _produceListMarker(state) - { - lines.appendText('*', Changeset.makeAttribsString('+', [ - ['list', state.listType], - ['insertorder', 'first'] - ], apool)); - } - cc.startNewLine = function(state) - { - if (state) - { - var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; - if (atBeginningOfLine && state.listType && state.listType != 'none') - { - _produceListMarker(state); - } - } - lines.startNew(); - } - cc.notifySelection = function(sel) - { - if (sel) - { - selection = sel; - startPoint = selection.startPoint; - endPoint = selection.endPoint; - } - }; - cc.doAttrib = function(state, na) - { - state.localAttribs = (state.localAttribs || []); - state.localAttribs.push(na); - cc.incrementAttrib(state, na); - }; - cc.collectContent = function(node, state) - { - if (!state) - { - state = { - flags: { /*name -> nesting counter*/ - }, - localAttribs: null, - attribs: { /*name -> nesting counter*/ - }, - attribString: '' - }; - } - var localAttribs = state.localAttribs; - state.localAttribs = null; - var isBlock = isBlockElement(node); - var isEmpty = _isEmpty(node, state); - if (isBlock) _ensureColumnZero(state); - var startLine = lines.length() - 1; - _reachBlockPoint(node, 0, state); - if (dom.isNodeText(node)) - { - var txt = dom.nodeValue(node); - var rest = ''; - var x = 0; // offset into original text - if (txt.length == 0) - { - if (startPoint && node == startPoint.node) - { - selStart = _pointHere(0, state); - } - if (endPoint && node == endPoint.node) - { - selEnd = _pointHere(0, state); - } - } - while (txt.length > 0) - { - var consumed = 0; - if (state.flags.preMode) - { - var firstLine = txt.split('\n', 1)[0]; - consumed = firstLine.length + 1; - rest = txt.substring(consumed); - txt = firstLine; - } - else - { /* will only run this loop body once */ - } - if (startPoint && node == startPoint.node && startPoint.index - x <= txt.length) - { - selStart = _pointHere(startPoint.index - x, state); - } - if (endPoint && node == endPoint.node && endPoint.index - x <= txt.length) - { - selEnd = _pointHere(endPoint.index - x, state); - } - var txt2 = txt; - if ((!state.flags.preMode) && /^[\r\n]*$/.exec(txt)) - { - // prevents textnodes containing just "\n" from being significant - // in safari when pasting text, now that we convert them to - // spaces instead of removing them, because in other cases - // removing "\n" from pasted HTML will collapse words together. - txt2 = ""; - } - var atBeginningOfLine = lines.textOfLine(lines.length() - 1).length == 0; - if (atBeginningOfLine) - { - // newlines in the source mustn't become spaces at beginning of line box - txt2 = txt2.replace(/^\n*/, ''); - } - if (atBeginningOfLine && state.listType && state.listType != 'none') - { - _produceListMarker(state); - } - lines.appendText(textify(txt2), state.attribString); - x += consumed; - txt = rest; - if (txt.length > 0) - { - cc.startNewLine(state); - } - } - } - else - { - var tname = (dom.nodeTagName(node) || "").toLowerCase(); - if (tname == "br") - { - cc.startNewLine(state); - } - else if (tname == "script" || tname == "style") - { - // ignore - } - else if (!isEmpty) - { - var styl = dom.nodeAttr(node, "style"); - var cls = dom.nodeProp(node, "className"); - - var isPre = (tname == "pre"); - if ((!isPre) && browser.safari) - { - isPre = (styl && /\bwhite-space:\s*pre\b/i.exec(styl)); - } - if (isPre) cc.incrementFlag(state, 'preMode'); - var oldListTypeOrNull = null; - var oldAuthorOrNull = null; - if (collectStyles) - { - plugins_.callHook('collectContentPre', { - cc: cc, - state: state, - tname: tname, - styl: styl, - cls: cls - }); - if (tname == "b" || (styl && /\bfont-weight:\s*bold\b/i.exec(styl)) || tname == "strong") - { - cc.doAttrib(state, "bold"); - } - if (tname == "i" || (styl && /\bfont-style:\s*italic\b/i.exec(styl)) || tname == "em") - { - cc.doAttrib(state, "italic"); - } - if (tname == "u" || (styl && /\btext-decoration:\s*underline\b/i.exec(styl)) || tname == "ins") - { - cc.doAttrib(state, "underline"); - } - if (tname == "s" || (styl && /\btext-decoration:\s*line-through\b/i.exec(styl)) || tname == "del") - { - cc.doAttrib(state, "strikethrough"); - } - if (tname == "ul" || tname == "ol") - { - var type; - var rr = cls && /(?:^| )list-([a-z]+[12345678])\b/.exec(cls); - type = rr && rr[1] || "bullet" + String(Math.min(_MAX_LIST_LEVEL, (state.listNesting || 0) + 1)); - oldListTypeOrNull = (_enterList(state, type) || 'none'); - } - else if ((tname == "div" || tname == "p") && cls && cls.match(/(?:^| )ace-line\b/)) - { - oldListTypeOrNull = (_enterList(state, type) || 'none'); - } - if (className2Author && cls) - { - var classes = cls.match(/\S+/g); - if (classes && classes.length > 0) - { - for (var i = 0; i < classes.length; i++) - { - var c = classes[i]; - var a = className2Author(c); - if (a) - { - oldAuthorOrNull = (_enterAuthor(state, a) || 'none'); - break; - } - } - } - } - } - - var nc = dom.nodeNumChildren(node); - for (var i = 0; i < nc; i++) - { - var c = dom.nodeChild(node, i); - cc.collectContent(c, state); - } - - if (collectStyles) - { - plugins_.callHook('collectContentPost', { - cc: cc, - state: state, - tname: tname, - styl: styl, - cls: cls - }); - } - - if (isPre) cc.decrementFlag(state, 'preMode'); - if (state.localAttribs) - { - for (var i = 0; i < state.localAttribs.length; i++) - { - cc.decrementAttrib(state, state.localAttribs[i]); - } - } - if (oldListTypeOrNull) - { - _exitList(state, oldListTypeOrNull); - } - if (oldAuthorOrNull) - { - _exitAuthor(state, oldAuthorOrNull); - } - } - } - if (!browser.msie) - { - _reachBlockPoint(node, 1, state); - } - if (isBlock) - { - if (lines.length() - 1 == startLine) - { - cc.startNewLine(state); - } - else - { - _ensureColumnZero(state); - } - } - - if (browser.msie) - { - // in IE, a point immediately after a DIV appears on the next line - _reachBlockPoint(node, 1, state); - } - - state.localAttribs = localAttribs; - }; - // can pass a falsy value for end of doc - cc.notifyNextNode = function(node) - { - // an "empty block" won't end a line; this addresses an issue in IE with - // typing into a blank line at the end of the document. typed text - // goes into the body, and the empty line div still looks clean. - // it is incorporated as dirty by the rule that a dirty region has - // to end a line. - if ((!node) || (isBlockElement(node) && !_isEmpty(node))) - { - _ensureColumnZero(null); - } - }; - // each returns [line, char] or [-1,-1] - var getSelectionStart = function() - { - return selStart; - }; - var getSelectionEnd = function() - { - return selEnd; - }; - - // returns array of strings for lines found, last entry will be "" if - // last line is complete (i.e. if a following span should be on a new line). - // can be called at any point - cc.getLines = function() - { - return lines.textLines(); - }; - - cc.finish = function() - { - lines.flush(); - var lineAttribs = lines.attribLines(); - var lineStrings = cc.getLines(); - - lineStrings.length--; - lineAttribs.length--; - - var ss = getSelectionStart(); - var se = getSelectionEnd(); - - function fixLongLines() - { - // design mode does not deal with with really long lines! - var lineLimit = 2000; // chars - var buffer = 10; // chars allowed over before wrapping - var linesWrapped = 0; - var numLinesAfter = 0; - for (var i = lineStrings.length - 1; i >= 0; i--) - { - var oldString = lineStrings[i]; - var oldAttribString = lineAttribs[i]; - if (oldString.length > lineLimit + buffer) - { - var newStrings = []; - var newAttribStrings = []; - while (oldString.length > lineLimit) - { - //var semiloc = oldString.lastIndexOf(';', lineLimit-1); - //var lengthToTake = (semiloc >= 0 ? (semiloc+1) : lineLimit); - lengthToTake = lineLimit; - newStrings.push(oldString.substring(0, lengthToTake)); - oldString = oldString.substring(lengthToTake); - newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake)); - oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake); - } - if (oldString.length > 0) - { - newStrings.push(oldString); - newAttribStrings.push(oldAttribString); - } - - function fixLineNumber(lineChar) - { - if (lineChar[0] < 0) return; - var n = lineChar[0]; - var c = lineChar[1]; - if (n > i) - { - n += (newStrings.length - 1); - } - else if (n == i) - { - var a = 0; - while (c > newStrings[a].length) - { - c -= newStrings[a].length; - a++; - } - n += a; - } - lineChar[0] = n; - lineChar[1] = c; - } - fixLineNumber(ss); - fixLineNumber(se); - linesWrapped++; - numLinesAfter += newStrings.length; - - newStrings.unshift(i, 1); - lineStrings.splice.apply(lineStrings, newStrings); - newAttribStrings.unshift(i, 1); - lineAttribs.splice.apply(lineAttribs, newAttribStrings); - } - } - return { - linesWrapped: linesWrapped, - numLinesAfter: numLinesAfter - }; - } - var wrapData = fixLongLines(); - - return { - selStart: ss, - selEnd: se, - linesWrapped: wrapData.linesWrapped, - numLinesAfter: wrapData.numLinesAfter, - lines: lineStrings, - lineAttribs: lineAttribs - }; - } - - return cc; -} - -exports.makeContentCollector = makeContentCollector; diff --git a/node/utils/tar.json b/node/utils/tar.json index b319791c..6a339e5c 100644 --- a/node/utils/tar.json +++ b/node/utils/tar.json @@ -39,6 +39,8 @@ , "pad_modals.js" , "pad_savedrevs.js" , "pad_impexp.js" + , "AttributePoolFactory.js" + , "Changeset.js" , "easysync2_client.js" , "domline_client.js" , "linestylefilter_client.js" @@ -50,6 +52,8 @@ ] , "ace2_inner.js": [ "ace2_common.js" + , "AttributePoolFactory.js" + , "Changeset.js" , "skiplist.js" , "virtual_lines.js" , "easysync2.js" diff --git a/node/utils/AttributePoolFactory.js b/static/js/AttributePoolFactory.js similarity index 97% rename from node/utils/AttributePoolFactory.js rename to static/js/AttributePoolFactory.js index 807c2b39..00b58dbb 100644 --- a/node/utils/AttributePoolFactory.js +++ b/static/js/AttributePoolFactory.js @@ -1,8 +1,8 @@ /** - * This code represents the Attribute Pool Object of the original Etherpad. + * This code represents the Attribute Pool Object of the original Etherpad. * 90% of the code is still like in the original Etherpad * Look at https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js - * You can find a explanation what a attribute pool is here: + * You can find a explanation what a attribute pool is here: * https://github.com/Pita/etherpad-lite/blob/master/doc/easysync/easysync-notes.txt */ diff --git a/node/utils/Changeset.js b/static/js/Changeset.js similarity index 99% rename from node/utils/Changeset.js rename to static/js/Changeset.js index 9e1b60eb..715836d5 100644 --- a/node/utils/Changeset.js +++ b/static/js/Changeset.js @@ -1,10 +1,10 @@ /* * This is the Changeset library copied from the old Etherpad with some modifications to use it in node.js * Can be found in https://github.com/ether/pad/blob/master/infrastructure/ace/www/easysync2.js - */ + */ /** - * This code is mostly from the old Etherpad. Please help us to comment this code. + * This code is mostly from the old Etherpad. Please help us to comment this code. * This helps other people to understand this code better and helps them to improve it. * TL;DR COMMENTS ON THIS FILE ARE HIGHLY APPRECIATED */ @@ -25,7 +25,7 @@ * limitations under the License. */ -var AttributePoolFactory = require("./AttributePoolFactory"); +var AttributePoolFactory = require("/AttributePoolFactory"); var _opt = null; diff --git a/static/js/contentcollector.js b/static/js/contentcollector.js index 0437ccd7..0964a8ed 100644 --- a/static/js/contentcollector.js +++ b/static/js/contentcollector.js @@ -25,7 +25,7 @@ var _MAX_LIST_LEVEL = 8; -var Changeset = require('/easysync2').Changeset +var Changeset = require('/easysync2').Changeset; var plugins = require('/plugins').plugins; function sanitizeUnicode(s) diff --git a/static/js/easysync2.js b/static/js/easysync2.js index cef868a1..632cc9ae 100644 --- a/static/js/easysync2.js +++ b/static/js/easysync2.js @@ -23,2491 +23,6 @@ */ //var _opt = (this.Easysync2Support || null); -var _opt = null; // disable optimization for now -function AttribPool() -{ - var p = {}; - p.numToAttrib = {}; // e.g. {0: ['foo','bar']} - p.attribToNum = {}; // e.g. {'foo,bar': 0} - p.nextNum = 0; - - p.putAttrib = function(attrib, dontAddIfAbsent) - { - var str = String(attrib); - if (str in p.attribToNum) - { - return p.attribToNum[str]; - } - if (dontAddIfAbsent) - { - return -1; - } - var num = p.nextNum++; - p.attribToNum[str] = num; - p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; - return num; - }; - - p.getAttrib = function(num) - { - var pair = p.numToAttrib[num]; - if (!pair) return pair; - return [pair[0], pair[1]]; // return a mutable copy - }; - - p.getAttribKey = function(num) - { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[0]; - }; - - p.getAttribValue = function(num) - { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[1]; - }; - - p.eachAttrib = function(func) - { - for (var n in p.numToAttrib) - { - var pair = p.numToAttrib[n]; - func(pair[0], pair[1]); - } - }; - - p.toJsonable = function() - { - return { - numToAttrib: p.numToAttrib, - nextNum: p.nextNum - }; - }; - - p.fromJsonable = function(obj) - { - p.numToAttrib = obj.numToAttrib; - p.nextNum = obj.nextNum; - p.attribToNum = {}; - for (var n in p.numToAttrib) - { - p.attribToNum[String(p.numToAttrib[n])] = Number(n); - } - return p; - }; - - return p; -} - -var Changeset = {}; - -Changeset.error = function error(msg) -{ - var e = new Error(msg); - e.easysync = true; - throw e; -}; -Changeset.assert = function assert(b, msgParts) -{ - if (!b) - { - var msg = Array.prototype.slice.call(arguments, 1).join(''); - Changeset.error("Changeset: " + msg); - } -}; - -Changeset.parseNum = function(str) -{ - return parseInt(str, 36); -}; -Changeset.numToString = function(num) -{ - return num.toString(36).toLowerCase(); -}; -Changeset.toBaseTen = function(cs) -{ - var dollarIndex = cs.indexOf('$'); - var beforeDollar = cs.substring(0, dollarIndex); - var fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, function(s) - { - return String(Changeset.parseNum(s)); - }) + fromDollar; -}; - -Changeset.oldLen = function(cs) -{ - return Changeset.unpack(cs).oldLen; -}; -Changeset.newLen = function(cs) -{ - return Changeset.unpack(cs).newLen; -}; - -Changeset.opIterator = function(opsStr, optStartIndex) -{ - //print(opsStr); - var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - var startIndex = (optStartIndex || 0); - var curIndex = startIndex; - var prevIndex = curIndex; - - function nextRegexMatch() - { - prevIndex = curIndex; - var result; - if (_opt) - { - result = _opt.nextOpInString(opsStr, curIndex); - if (result) - { - if (result.opcode() == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - curIndex = result.lastIndex(); - } - } - else - { - regex.lastIndex = curIndex; - result = regex.exec(opsStr); - curIndex = regex.lastIndex; - if (result[0] == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - } - return result; - } - var regexResult = nextRegexMatch(); - var obj = Changeset.newOp(); - - function next(optObj) - { - var op = (optObj || obj); - if (_opt && regexResult) - { - op.attribs = regexResult.attribs(); - op.lines = regexResult.lines(); - op.chars = regexResult.chars(); - op.opcode = regexResult.opcode(); - regexResult = nextRegexMatch(); - } - else if ((!_opt) && regexResult[0]) - { - op.attribs = regexResult[1]; - op.lines = Changeset.parseNum(regexResult[2] || 0); - op.opcode = regexResult[3]; - op.chars = Changeset.parseNum(regexResult[4]); - regexResult = nextRegexMatch(); - } - else - { - Changeset.clearOp(op); - } - return op; - } - - function hasNext() - { - return !!(_opt ? regexResult : regexResult[0]); - } - - function lastIndex() - { - return prevIndex; - } - return { - next: next, - hasNext: hasNext, - lastIndex: lastIndex - }; -}; - -Changeset.clearOp = function(op) -{ - op.opcode = ''; - op.chars = 0; - op.lines = 0; - op.attribs = ''; -}; -Changeset.newOp = function(optOpcode) -{ - return { - opcode: (optOpcode || ''), - chars: 0, - lines: 0, - attribs: '' - }; -}; -Changeset.cloneOp = function(op) -{ - return { - opcode: op.opcode, - chars: op.chars, - lines: op.lines, - attribs: op.attribs - }; -}; -Changeset.copyOp = function(op1, op2) -{ - op2.opcode = op1.opcode; - op2.chars = op1.chars; - op2.lines = op1.lines; - op2.attribs = op1.attribs; -}; -Changeset.opString = function(op) -{ - // just for debugging - if (!op.opcode) return 'null'; - var assem = Changeset.opAssembler(); - assem.append(op); - return assem.toString(); -}; -Changeset.stringOp = function(str) -{ - // just for debugging - return Changeset.opIterator(str).next(); -}; - -Changeset.checkRep = function(cs) -{ - // doesn't check things that require access to attrib pool (e.g. attribute order) - // or original string (e.g. newline positions) - var unpacked = Changeset.unpack(cs); - var oldLen = unpacked.oldLen; - var newLen = unpacked.newLen; - var ops = unpacked.ops; - var charBank = unpacked.charBank; - - var assem = Changeset.smartOpAssembler(); - var oldPos = 0; - var calcNewLen = 0; - var numInserted = 0; - var iter = Changeset.opIterator(ops); - while (iter.hasNext()) - { - var o = iter.next(); - switch (o.opcode) - { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - Changeset.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs); - break; - case '+': - { - calcNewLen += o.chars; - numInserted += o.chars; - Changeset.assert(calcNewLen < newLen, calcNewLen, " >= ", newLen, " in ", cs); - break; - } - } - assem.append(o); - } - - calcNewLen += oldLen - oldPos; - charBank = charBank.substring(0, numInserted); - while (charBank.length < numInserted) - { - charBank += "?"; - } - - assem.endDocument(); - var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); - Changeset.assert(normalized == cs, normalized, ' != ', cs); - - return cs; -} - -Changeset.smartOpAssembler = function() -{ - // Like opAssembler but able to produce conforming changesets - // from slightly looser input, at the cost of speed. - // Specifically: - // - merges consecutive operations that can be merged - // - strips final "=" - // - ignores 0-length changes - // - reorders consecutive + and - (which margingOpAssembler doesn't do) - var minusAssem = Changeset.mergingOpAssembler(); - var plusAssem = Changeset.mergingOpAssembler(); - var keepAssem = Changeset.mergingOpAssembler(); - var assem = Changeset.stringAssembler(); - var lastOpcode = ''; - var lengthChange = 0; - - function flushKeeps() - { - assem.append(keepAssem.toString()); - keepAssem.clear(); - } - - function flushPlusMinus() - { - assem.append(minusAssem.toString()); - minusAssem.clear(); - assem.append(plusAssem.toString()); - plusAssem.clear(); - } - - function append(op) - { - if (!op.opcode) return; - if (!op.chars) return; - - if (op.opcode == '-') - { - if (lastOpcode == '=') - { - flushKeeps(); - } - minusAssem.append(op); - lengthChange -= op.chars; - } - else if (op.opcode == '+') - { - if (lastOpcode == '=') - { - flushKeeps(); - } - plusAssem.append(op); - lengthChange += op.chars; - } - else if (op.opcode == '=') - { - if (lastOpcode != '=') - { - flushPlusMinus(); - } - keepAssem.append(op); - } - lastOpcode = op.opcode; - } - - function appendOpWithText(opcode, text, attribs, pool) - { - var op = Changeset.newOp(opcode); - op.attribs = Changeset.makeAttribsString(opcode, attribs, pool); - var lastNewlinePos = text.lastIndexOf('\n'); - if (lastNewlinePos < 0) - { - op.chars = text.length; - op.lines = 0; - append(op); - } - else - { - op.chars = lastNewlinePos + 1; - op.lines = text.match(/\n/g).length; - append(op); - op.chars = text.length - (lastNewlinePos + 1); - op.lines = 0; - append(op); - } - } - - function toString() - { - flushPlusMinus(); - flushKeeps(); - return assem.toString(); - } - - function clear() - { - minusAssem.clear(); - plusAssem.clear(); - keepAssem.clear(); - assem.clear(); - lengthChange = 0; - } - - function endDocument() - { - keepAssem.endDocument(); - } - - function getLengthChange() - { - return lengthChange; - } - - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument, - appendOpWithText: appendOpWithText, - getLengthChange: getLengthChange - }; -}; - -if (_opt) -{ - Changeset.mergingOpAssembler = function() - { - var assem = _opt.mergingOpAssembler(); - - function append(op) - { - assem.append(op.opcode, op.chars, op.lines, op.attribs); - } - - function toString() - { - return assem.toString(); - } - - function clear() - { - assem.clear(); - } - - function endDocument() - { - assem.endDocument(); - } - - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument - }; - }; -} -else -{ - Changeset.mergingOpAssembler = function() - { - // This assembler can be used in production; it efficiently - // merges consecutive operations that are mergeable, ignores - // no-ops, and drops final pure "keeps". It does not re-order - // operations. - var assem = Changeset.opAssembler(); - var bufOp = Changeset.newOp(); - - // If we get, for example, insertions [xxx\n,yyy], those don't merge, - // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. - // This variable stores the length of yyy and any other newline-less - // ops immediately after it. - var bufOpAdditionalCharsAfterNewline = 0; - - function flush(isEndDocument) - { - if (bufOp.opcode) - { - if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) - { - // final merged keep, leave it implicit - } - else - { - assem.append(bufOp); - if (bufOpAdditionalCharsAfterNewline) - { - bufOp.chars = bufOpAdditionalCharsAfterNewline; - bufOp.lines = 0; - assem.append(bufOp); - bufOpAdditionalCharsAfterNewline = 0; - } - } - bufOp.opcode = ''; - } - } - - function append(op) - { - if (op.chars > 0) - { - if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) - { - if (op.lines > 0) - { - // bufOp and additional chars are all mergeable into a multi-line op - bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; - bufOp.lines += op.lines; - bufOpAdditionalCharsAfterNewline = 0; - } - else if (bufOp.lines == 0) - { - // both bufOp and op are in-line - bufOp.chars += op.chars; - } - else - { - // append in-line text to multi-line bufOp - bufOpAdditionalCharsAfterNewline += op.chars; - } - } - else - { - flush(); - Changeset.copyOp(op, bufOp); - } - } - } - - function endDocument() - { - flush(true); - } - - function toString() - { - flush(); - return assem.toString(); - } - - function clear() - { - assem.clear(); - Changeset.clearOp(bufOp); - } - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument - }; - }; -} - -if (_opt) -{ - Changeset.opAssembler = function() - { - var assem = _opt.opAssembler(); - // this function allows op to be mutated later (doesn't keep a ref) - - - function append(op) - { - assem.append(op.opcode, op.chars, op.lines, op.attribs); - } - - function toString() - { - return assem.toString(); - } - - function clear() - { - assem.clear(); - } - return { - append: append, - toString: toString, - clear: clear - }; - }; -} -else -{ - Changeset.opAssembler = function() - { - var pieces = []; - // this function allows op to be mutated later (doesn't keep a ref) - - - function append(op) - { - pieces.push(op.attribs); - if (op.lines) - { - pieces.push('|', Changeset.numToString(op.lines)); - } - pieces.push(op.opcode); - pieces.push(Changeset.numToString(op.chars)); - } - - function toString() - { - return pieces.join(''); - } - - function clear() - { - pieces.length = 0; - } - return { - append: append, - toString: toString, - clear: clear - }; - }; -} - -Changeset.stringIterator = function(str) -{ - var curIndex = 0; - - function assertRemaining(n) - { - Changeset.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")"); - } - - function take(n) - { - assertRemaining(n); - var s = str.substr(curIndex, n); - curIndex += n; - return s; - } - - function peek(n) - { - assertRemaining(n); - var s = str.substr(curIndex, n); - return s; - } - - function skip(n) - { - assertRemaining(n); - curIndex += n; - } - - function remaining() - { - return str.length - curIndex; - } - return { - take: take, - skip: skip, - remaining: remaining, - peek: peek - }; -}; - -Changeset.stringAssembler = function() -{ - var pieces = []; - - function append(x) - { - pieces.push(String(x)); - } - - function toString() - { - return pieces.join(''); - } - return { - append: append, - toString: toString - }; -}; - -// "lines" need not be an array as long as it supports certain calls (lines_foo inside). -Changeset.textLinesMutator = function(lines) -{ - // Mutates lines, an array of strings, in place. - // Mutation operations have the same constraints as changeset operations - // with respect to newlines, but not the other additional constraints - // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). - // Can be used to mutate lists of strings where the last char of each string - // is not actually a newline, but for the purposes of N and L values, - // the caller should pretend it is, and for things to work right in that case, the input - // to insert() should be a single line with no newlines. - var curSplice = [0, 0]; - var inSplice = false; - // position in document after curSplice is applied: - var curLine = 0, - curCol = 0; - // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && - // curLine >= curSplice[0] - // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then - // curCol == 0 - - function lines_applySplice(s) - { - lines.splice.apply(lines, s); - } - - function lines_toSource() - { - return lines.toSource(); - } - - function lines_get(idx) - { - if (lines.get) - { - return lines.get(idx); - } - else - { - return lines[idx]; - } - } - // can be unimplemented if removeLines's return value not needed - - - function lines_slice(start, end) - { - if (lines.slice) - { - return lines.slice(start, end); - } - else - { - return []; - } - } - - function lines_length() - { - if ((typeof lines.length) == "number") - { - return lines.length; - } - else - { - return lines.length(); - } - } - - function enterSplice() - { - curSplice[0] = curLine; - curSplice[1] = 0; - if (curCol > 0) - { - putCurLineInSplice(); - } - inSplice = true; - } - - function leaveSplice() - { - lines_applySplice(curSplice); - curSplice.length = 2; - curSplice[0] = curSplice[1] = 0; - inSplice = false; - } - - function isCurLineInSplice() - { - return (curLine - curSplice[0] < (curSplice.length - 2)); - } - - function debugPrint(typ) - { - print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource()); - } - - function putCurLineInSplice() - { - if (!isCurLineInSplice()) - { - curSplice.push(lines_get(curSplice[0] + curSplice[1])); - curSplice[1]++; - } - return 2 + curLine - curSplice[0]; - } - - function skipLines(L, includeInSplice) - { - if (L) - { - if (includeInSplice) - { - if (!inSplice) - { - enterSplice(); - } - for (var i = 0; i < L; i++) - { - curCol = 0; - putCurLineInSplice(); - curLine++; - } - } - else - { - if (inSplice) - { - if (L > 1) - { - leaveSplice(); - } - else - { - putCurLineInSplice(); - } - } - curLine += L; - curCol = 0; - } - //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); -/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { - print("BLAH"); - putCurLineInSplice(); - }*/ - // tests case foo in remove(), which isn't otherwise covered in current impl - } - //debugPrint("skip"); - } - - function skip(N, L, includeInSplice) - { - if (N) - { - if (L) - { - skipLines(L, includeInSplice); - } - else - { - if (includeInSplice && !inSplice) - { - enterSplice(); - } - if (inSplice) - { - putCurLineInSplice(); - } - curCol += N; - //debugPrint("skip"); - } - } - } - - function removeLines(L) - { - var removed = ''; - if (L) - { - if (!inSplice) - { - enterSplice(); - } - - function nextKLinesText(k) - { - var m = curSplice[0] + curSplice[1]; - return lines_slice(m, m + k).join(''); - } - if (isCurLineInSplice()) - { - //print(curCol); - if (curCol == 0) - { - removed = curSplice[curSplice.length - 1]; - // print("FOO"); // case foo - curSplice.length--; - removed += nextKLinesText(L - 1); - curSplice[1] += L - 1; - } - else - { - removed = nextKLinesText(L - 1); - curSplice[1] += L - 1; - var sline = curSplice.length - 1; - removed = curSplice[sline].substring(curCol) + removed; - curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]); - curSplice[1] += 1; - } - } - else - { - removed = nextKLinesText(L); - curSplice[1] += L; - } - //debugPrint("remove"); - } - return removed; - } - - function remove(N, L) - { - var removed = ''; - if (N) - { - if (L) - { - return removeLines(L); - } - else - { - if (!inSplice) - { - enterSplice(); - } - var sline = putCurLineInSplice(); - removed = curSplice[sline].substring(curCol, curCol + N); - curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); - //debugPrint("remove"); - } - } - return removed; - } - - function insert(text, L) - { - if (text) - { - if (!inSplice) - { - enterSplice(); - } - if (L) - { - var newLines = Changeset.splitTextLines(text); - if (isCurLineInSplice()) - { - //if (curCol == 0) { - //curSplice.length--; - //curSplice[1]--; - //Array.prototype.push.apply(curSplice, newLines); - //curLine += newLines.length; - //} - //else { - var sline = curSplice.length - 1; - var theLine = curSplice[sline]; - var lineCol = curCol; - curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; - curLine++; - newLines.splice(0, 1); - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - curSplice.push(theLine.substring(lineCol)); - curCol = 0; - //} - } - else - { - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - } - } - else - { - var sline = putCurLineInSplice(); - curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol); - curCol += text.length; - } - //debugPrint("insert"); - } - } - - function hasMore() - { - //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); - var docLines = lines_length(); - if (inSplice) - { - docLines += curSplice.length - 2 - curSplice[1]; - } - return curLine < docLines; - } - - function close() - { - if (inSplice) - { - leaveSplice(); - } - //debugPrint("close"); - } - - var self = { - skip: skip, - remove: remove, - insert: insert, - close: close, - hasMore: hasMore, - removeLines: removeLines, - skipLines: skipLines - }; - return self; -}; - -Changeset.applyZip = function(in1, idx1, in2, idx2, func) -{ - var iter1 = Changeset.opIterator(in1, idx1); - var iter2 = Changeset.opIterator(in2, idx2); - var assem = Changeset.smartOpAssembler(); - var op1 = Changeset.newOp(); - var op2 = Changeset.newOp(); - var opOut = Changeset.newOp(); - while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) - { - if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); - if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); - func(op1, op2, opOut); - if (opOut.opcode) - { - //print(opOut.toSource()); - assem.append(opOut); - opOut.opcode = ''; - } - } - assem.endDocument(); - return assem.toString(); -}; - -Changeset.unpack = function(cs) -{ - var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - var headerMatch = headerRegex.exec(cs); - if ((!headerMatch) || (!headerMatch[0])) - { - Changeset.error("Not a changeset: " + cs); - } - var oldLen = Changeset.parseNum(headerMatch[1]); - var changeSign = (headerMatch[2] == '>') ? 1 : -1; - var changeMag = Changeset.parseNum(headerMatch[3]); - var newLen = oldLen + changeSign * changeMag; - var opsStart = headerMatch[0].length; - var opsEnd = cs.indexOf("$"); - if (opsEnd < 0) opsEnd = cs.length; - return { - oldLen: oldLen, - newLen: newLen, - ops: cs.substring(opsStart, opsEnd), - charBank: cs.substring(opsEnd + 1) - }; -}; - -Changeset.pack = function(oldLen, newLen, opsStr, bank) -{ - var lenDiff = newLen - oldLen; - var lenDiffStr = (lenDiff >= 0 ? '>' + Changeset.numToString(lenDiff) : '<' + Changeset.numToString(-lenDiff)); - var a = []; - a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); - return a.join(''); -}; - -Changeset.applyToText = function(cs, str) -{ - var unpacked = Changeset.unpack(cs); - Changeset.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var strIter = Changeset.stringIterator(str); - var assem = Changeset.stringAssembler(); - while (csIter.hasNext()) - { - var op = csIter.next(); - switch (op.opcode) - { - case '+': - assem.append(bankIter.take(op.chars)); - break; - case '-': - strIter.skip(op.chars); - break; - case '=': - assem.append(strIter.take(op.chars)); - break; - } - } - assem.append(strIter.take(strIter.remaining())); - return assem.toString(); -}; - -Changeset.mutateTextLines = function(cs, lines) -{ - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var mut = Changeset.textLinesMutator(lines); - while (csIter.hasNext()) - { - var op = csIter.next(); - switch (op.opcode) - { - case '+': - mut.insert(bankIter.take(op.chars), op.lines); - break; - case '-': - mut.remove(op.chars, op.lines); - break; - case '=': - mut.skip(op.chars, op.lines, ( !! op.attribs)); - break; - } - } - mut.close(); -}; - -Changeset.composeAttributes = function(att1, att2, resultIsMutation, pool) -{ - // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. - // Sometimes attribute (key,value) pairs are treated as attribute presence - // information, while other times they are treated as operations that - // mutate a set of attributes, and this affects whether an empty value - // is a deletion or a change. - // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result - // ([], [(bold, )], true) -> [(bold, )] - // ([], [(bold, )], false) -> [] - // ([], [(bold, true)], true) -> [(bold, true)] - // ([], [(bold, true)], false) -> [(bold, true)] - // ([(bold, true)], [(bold, )], true) -> [(bold, )] - // ([(bold, true)], [(bold, )], false) -> [] - // pool can be null if att2 has no attributes. - if ((!att1) && resultIsMutation) - { - // In the case of a mutation (i.e. composing two changesets), - // an att2 composed with an empy att1 is just att2. If att1 - // is part of an attribution string, then att2 may remove - // attributes that are already gone, so don't do this optimization. - return att2; - } - if (!att2) return att1; - var atts = []; - att1.replace(/\*([0-9a-z]+)/g, function(_, a) - { - atts.push(pool.getAttrib(Changeset.parseNum(a))); - return ''; - }); - att2.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var pair = pool.getAttrib(Changeset.parseNum(a)); - var found = false; - for (var i = 0; i < atts.length; i++) - { - var oldPair = atts[i]; - if (oldPair[0] == pair[0]) - { - if (pair[1] || resultIsMutation) - { - oldPair[1] = pair[1]; - } - else - { - atts.splice(i, 1); - } - found = true; - break; - } - } - if ((!found) && (pair[1] || resultIsMutation)) - { - atts.push(pair); - } - return ''; - }); - atts.sort(); - var buf = Changeset.stringAssembler(); - for (var i = 0; i < atts.length; i++) - { - buf.append('*'); - buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); - } - //print(att1+" / "+att2+" / "+buf.toString()); - return buf.toString(); -}; - -Changeset._slicerZipperFunc = function(attOp, csOp, opOut, pool) -{ - // attOp is the op from the sequence that is being operated on, either an - // attribution string or the earlier of two changesets being composed. - // pool can be null if definitely not needed. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - if (attOp.opcode == '-') - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - } - else if (!attOp.opcode) - { - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - } - else - { - switch (csOp.opcode) - { - case '-': - { - if (csOp.chars <= attOp.chars) - { - // delete or delete part - if (attOp.opcode == '=') - { - opOut.opcode = '-'; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = ''; - } - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - csOp.opcode = ''; - if (!attOp.chars) - { - attOp.opcode = ''; - } - } - else - { - // delete and keep going - if (attOp.opcode == '=') - { - opOut.opcode = '-'; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = ''; - } - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - attOp.opcode = ''; - } - break; - } - case '+': - { - // insert - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - break; - } - case '=': - { - if (csOp.chars <= attOp.chars) - { - // keep or keep part - opOut.opcode = attOp.opcode; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); - csOp.opcode = ''; - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - if (!attOp.chars) - { - attOp.opcode = ''; - } - } - else - { - // keep and keep going - opOut.opcode = attOp.opcode; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } - } -}; - -Changeset.applyToAttribution = function(cs, astr, pool) -{ - var unpacked = Changeset.unpack(cs); - - return Changeset.applyZip(astr, 0, unpacked.ops, 0, function(op1, op2, opOut) - { - return Changeset._slicerZipperFunc(op1, op2, opOut, pool); - }); -}; - -/*Changeset.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { - var iter = Changeset.opIterator(opsStr, optStartIndex); - var bankIndex = 0; - -};*/ - -Changeset.mutateAttributionLines = function(cs, lines, pool) -{ - //dmesg(cs); - //dmesg(lines.toSource()+" ->"); - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var csBank = unpacked.charBank; - var csBankIndex = 0; - // treat the attribution lines as text lines, mutating a line at a time - var mut = Changeset.textLinesMutator(lines); - - var lineIter = null; - - function isNextMutOp() - { - return (lineIter && lineIter.hasNext()) || mut.hasMore(); - } - - function nextMutOp(destOp) - { - if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) - { - var line = mut.removeLines(1); - lineIter = Changeset.opIterator(line); - } - if (lineIter && lineIter.hasNext()) - { - lineIter.next(destOp); - } - else - { - destOp.opcode = ''; - } - } - var lineAssem = null; - - function outputMutOp(op) - { - //print("outputMutOp: "+op.toSource()); - if (!lineAssem) - { - lineAssem = Changeset.mergingOpAssembler(); - } - lineAssem.append(op); - if (op.lines > 0) - { - Changeset.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines"); - // ship it to the mut - mut.insert(lineAssem.toString(), 1); - lineAssem = null; - } - } - - var csOp = Changeset.newOp(); - var attOp = Changeset.newOp(); - var opOut = Changeset.newOp(); - while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) - { - if ((!csOp.opcode) && csIter.hasNext()) - { - csIter.next(csOp); - } - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); - //print("csOp: "+csOp.toSource()); - if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) - { - break; // done - } - else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) - { - // skip multiple lines; this is what makes small changes not order of the document size - mut.skipLines(csOp.lines); - //print("skipped: "+csOp.lines); - csOp.opcode = ''; - } - else if (csOp.opcode == '+') - { - if (csOp.lines > 1) - { - var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; - Changeset.copyOp(csOp, opOut); - csOp.chars -= firstLineLen; - csOp.lines--; - opOut.lines = 1; - opOut.chars = firstLineLen; - } - else - { - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - } - outputMutOp(opOut); - csBankIndex += opOut.chars; - opOut.opcode = ''; - } - else - { - if ((!attOp.opcode) && isNextMutOp()) - { - nextMutOp(attOp); - } - //print("attOp: "+attOp.toSource()); - Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); - if (opOut.opcode) - { - outputMutOp(opOut); - opOut.opcode = ''; - } - } - } - - Changeset.assert(!lineAssem, "line assembler not finished"); - mut.close(); - - //dmesg("-> "+lines.toSource()); -}; - -Changeset.joinAttributionLines = function(theAlines) -{ - var assem = Changeset.mergingOpAssembler(); - for (var i = 0; i < theAlines.length; i++) - { - var aline = theAlines[i]; - var iter = Changeset.opIterator(aline); - while (iter.hasNext()) - { - assem.append(iter.next()); - } - } - return assem.toString(); -}; - -Changeset.splitAttributionLines = function(attrOps, text) -{ - var iter = Changeset.opIterator(attrOps); - var assem = Changeset.mergingOpAssembler(); - var lines = []; - var pos = 0; - - function appendOp(op) - { - assem.append(op); - if (op.lines > 0) - { - lines.push(assem.toString()); - assem.clear(); - } - pos += op.chars; - } - - while (iter.hasNext()) - { - var op = iter.next(); - var numChars = op.chars; - var numLines = op.lines; - while (numLines > 1) - { - var newlineEnd = text.indexOf('\n', pos) + 1; - Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); - op.chars = newlineEnd - pos; - op.lines = 1; - appendOp(op); - numChars -= op.chars; - numLines -= op.lines; - } - if (numLines == 1) - { - op.chars = numChars; - op.lines = 1; - } - appendOp(op); - } - - return lines; -}; - -Changeset.splitTextLines = function(text) -{ - return text.match(/[^\n]*(?:\n|[^\n]$)/g); -}; - -Changeset.compose = function(cs1, cs2, pool) -{ - var unpacked1 = Changeset.unpack(cs1); - var unpacked2 = Changeset.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); - var len3 = unpacked2.newLen; - var bankIter1 = Changeset.stringIterator(unpacked1.charBank); - var bankIter2 = Changeset.stringIterator(unpacked2.charBank); - var bankAssem = Changeset.stringAssembler(); - - var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) - { - //var debugBuilder = Changeset.stringAssembler(); - //debugBuilder.append(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' / '); - var op1code = op1.opcode; - var op2code = op2.opcode; - if (op1code == '+' && op2code == '-') - { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - Changeset._slicerZipperFunc(op1, op2, opOut, pool); - if (opOut.opcode == '+') - { - if (op2code == '+') - { - bankAssem.append(bankIter2.take(opOut.chars)); - } - else - { - bankAssem.append(bankIter1.take(opOut.chars)); - } - } - - //debugBuilder.append(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' -> '); - //debugBuilder.append(Changeset.opString(opOut)); - //print(debugBuilder.toString()); - }); - - return Changeset.pack(len1, len3, newOps, bankAssem.toString()); -}; - -Changeset.attributeTester = function(attribPair, pool) -{ - // returns a function that tests if a string of attributes - // (e.g. *3*4) contains a given attribute key,value that - // is already present in the pool. - if (!pool) - { - return never; - } - var attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) - { - return never; - } - else - { - var re = new RegExp('\\*' + Changeset.numToString(attribNum) + '(?!\\w)'); - return function(attribs) - { - return re.test(attribs); - }; - } - - function never(attribs) - { - return false; - } -}; - -Changeset.identity = function(N) -{ - return Changeset.pack(N, N, "", ""); -}; - -Changeset.makeSplice = function(oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) -{ - var oldLen = oldFullText.length; - - if (spliceStart >= oldLen) - { - spliceStart = oldLen - 1; - } - if (numRemoved > oldFullText.length - spliceStart - 1) - { - numRemoved = oldFullText.length - spliceStart - 1; - } - var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); - var newLen = oldLen + newText.length - oldText.length; - - var assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); - assem.appendOpWithText('-', oldText); - assem.appendOpWithText('+', newText, optNewTextAPairs, pool); - assem.endDocument(); - return Changeset.pack(oldLen, newLen, assem.toString(), newText); -}; - -Changeset.toSplices = function(cs) -{ - // get a list of splices, [startChar, endChar, newText] - var unpacked = Changeset.unpack(cs); - var splices = []; - - var oldPos = 0; - var iter = Changeset.opIterator(unpacked.ops); - var charIter = Changeset.stringIterator(unpacked.charBank); - var inSplice = false; - while (iter.hasNext()) - { - var op = iter.next(); - if (op.opcode == '=') - { - oldPos += op.chars; - inSplice = false; - } - else - { - if (!inSplice) - { - splices.push([oldPos, oldPos, ""]); - inSplice = true; - } - if (op.opcode == '-') - { - oldPos += op.chars; - splices[splices.length - 1][1] += op.chars; - } - else if (op.opcode == '+') - { - splices[splices.length - 1][2] += charIter.take(op.chars); - } - } - } - - return splices; -}; - -Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) -{ - var newStartChar = startChar; - var newEndChar = endChar; - var splices = Changeset.toSplices(cs); - var lengthChangeSoFar = 0; - for (var i = 0; i < splices.length; i++) - { - var splice = splices[i]; - var spliceStart = splice[0] + lengthChangeSoFar; - var spliceEnd = splice[1] + lengthChangeSoFar; - var newTextLength = splice[2].length; - var thisLengthChange = newTextLength - (spliceEnd - spliceStart); - - if (spliceStart <= newStartChar && spliceEnd >= newEndChar) - { - // splice fully replaces/deletes range - // (also case that handles insertion at a collapsed selection) - if (insertionsAfter) - { - newStartChar = newEndChar = spliceStart; - } - else - { - newStartChar = newEndChar = spliceStart + newTextLength; - } - } - else if (spliceEnd <= newStartChar) - { - // splice is before range - newStartChar += thisLengthChange; - newEndChar += thisLengthChange; - } - else if (spliceStart >= newEndChar) - { - // splice is after range - } - else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) - { - // splice is inside range - newEndChar += thisLengthChange; - } - else if (spliceEnd < newEndChar) - { - // splice overlaps beginning of range - newStartChar = spliceStart + newTextLength; - newEndChar += thisLengthChange; - } - else - { - // splice overlaps end of range - newEndChar = spliceStart; - } - - lengthChangeSoFar += thisLengthChange; - } - - return [newStartChar, newEndChar]; -}; - -Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) -{ - // works on changeset or attribution string - var dollarPos = cs.indexOf('$'); - if (dollarPos < 0) - { - dollarPos = cs.length; - } - var upToDollar = cs.substring(0, dollarPos); - var fromDollar = cs.substring(dollarPos); - // order of attribs stays the same - return upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var oldNum = Changeset.parseNum(a); - var pair = oldPool.getAttrib(oldNum); - var newNum = newPool.putAttrib(pair); - return '*' + Changeset.numToString(newNum); - }) + fromDollar; -}; - -Changeset.makeAttribution = function(text) -{ - var assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('+', text); - return assem.toString(); -}; - -// callable on a changeset, attribution string, or attribs property of an op -Changeset.eachAttribNumber = function(cs, func) -{ - var dollarPos = cs.indexOf('$'); - if (dollarPos < 0) - { - dollarPos = cs.length; - } - var upToDollar = cs.substring(0, dollarPos); - - upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) - { - func(Changeset.parseNum(a)); - return ''; - }); -}; - -// callable on a changeset, attribution string, or attribs property of an op, -// though it may easily create adjacent ops that can be merged. -Changeset.filterAttribNumbers = function(cs, filter) -{ - return Changeset.mapAttribNumbers(cs, filter); -}; - -Changeset.mapAttribNumbers = function(cs, func) -{ - var dollarPos = cs.indexOf('$'); - if (dollarPos < 0) - { - dollarPos = cs.length; - } - var upToDollar = cs.substring(0, dollarPos); - - var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function(s, a) - { - var n = func(Changeset.parseNum(a)); - if (n === true) - { - return s; - } - else if ((typeof n) === "number") - { - return '*' + Changeset.numToString(n); - } - else - { - return ''; - } - }); - - return newUpToDollar + cs.substring(dollarPos); -}; - -Changeset.makeAText = function(text, attribs) -{ - return { - text: text, - attribs: (attribs || Changeset.makeAttribution(text)) - }; -}; - -Changeset.applyToAText = function(cs, atext, pool) -{ - return { - text: Changeset.applyToText(cs, atext.text), - attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) - }; -}; - -Changeset.cloneAText = function(atext) -{ - return { - text: atext.text, - attribs: atext.attribs - }; -}; - -Changeset.copyAText = function(atext1, atext2) -{ - atext2.text = atext1.text; - atext2.attribs = atext1.attribs; -}; - -Changeset.appendATextToAssembler = function(atext, assem) -{ - // intentionally skips last newline char of atext - var iter = Changeset.opIterator(atext.attribs); - var op = Changeset.newOp(); - while (iter.hasNext()) - { - iter.next(op); - if (!iter.hasNext()) - { - // last op, exclude final newline - if (op.lines <= 1) - { - op.lines = 0; - op.chars--; - if (op.chars) - { - assem.append(op); - } - } - else - { - var nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; - var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; - op.lines--; - op.chars -= (lastLineLength + 1); - assem.append(op); - op.lines = 0; - op.chars = lastLineLength; - if (op.chars) - { - assem.append(op); - } - } - } - else - { - assem.append(op); - } - } -}; - -Changeset.prepareForWire = function(cs, pool) -{ - var newPool = new AttribPool(); - var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); - return { - translated: newCs, - pool: newPool - }; -}; - -Changeset.isIdentity = function(cs) -{ - var unpacked = Changeset.unpack(cs); - return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; -}; - -Changeset.opAttributeValue = function(op, key, pool) -{ - return Changeset.attribsAttributeValue(op.attribs, key, pool); -}; - -Changeset.attribsAttributeValue = function(attribs, key, pool) -{ - var value = ''; - if (attribs) - { - Changeset.eachAttribNumber(attribs, function(n) - { - if (pool.getAttribKey(n) == key) - { - value = pool.getAttribValue(n); - } - }); - } - return value; -}; - -Changeset.builder = function(oldLen) -{ - var assem = Changeset.smartOpAssembler(); - var o = Changeset.newOp(); - var charBank = Changeset.stringAssembler(); - - var self = { - // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) - keep: function(N, L, attribs, pool) - { - o.opcode = '='; - o.attribs = (attribs && Changeset.makeAttribsString('=', attribs, pool)) || ''; - o.chars = N; - o.lines = (L || 0); - assem.append(o); - return self; - }, - keepText: function(text, attribs, pool) - { - assem.appendOpWithText('=', text, attribs, pool); - return self; - }, - insert: function(text, attribs, pool) - { - assem.appendOpWithText('+', text, attribs, pool); - charBank.append(text); - return self; - }, - remove: function(N, L) - { - o.opcode = '-'; - o.attribs = ''; - o.chars = N; - o.lines = (L || 0); - assem.append(o); - return self; - }, - toString: function() - { - assem.endDocument(); - var newLen = oldLen + assem.getLengthChange(); - return Changeset.pack(oldLen, newLen, assem.toString(), charBank.toString()); - } - }; - - return self; -}; - -Changeset.makeAttribsString = function(opcode, attribs, pool) -{ - // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work - if (!attribs) - { - return ''; - } - else if ((typeof attribs) == "string") - { - return attribs; - } - else if (pool && attribs && attribs.length) - { - if (attribs.length > 1) - { - attribs = attribs.slice(); - attribs.sort(); - } - var result = []; - for (var i = 0; i < attribs.length; i++) - { - var pair = attribs[i]; - if (opcode == '=' || (opcode == '+' && pair[1])) - { - result.push('*' + Changeset.numToString(pool.putAttrib(pair))); - } - } - return result.join(''); - } -}; - -// like "substring" but on a single-line attribution string -Changeset.subattribution = function(astr, start, optEnd) -{ - var iter = Changeset.opIterator(astr, 0); - var assem = Changeset.smartOpAssembler(); - var attOp = Changeset.newOp(); - var csOp = Changeset.newOp(); - var opOut = Changeset.newOp(); - - function doCsOp() - { - if (csOp.chars) - { - while (csOp.opcode && (attOp.opcode || iter.hasNext())) - { - if (!attOp.opcode) iter.next(attOp); - - if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) - { - csOp.lines++; - } - - Changeset._slicerZipperFunc(attOp, csOp, opOut, null); - if (opOut.opcode) - { - assem.append(opOut); - opOut.opcode = ''; - } - } - } - } - - csOp.opcode = '-'; - csOp.chars = start; - - doCsOp(); - - if (optEnd === undefined) - { - if (attOp.opcode) - { - assem.append(attOp); - } - while (iter.hasNext()) - { - iter.next(attOp); - assem.append(attOp); - } - } - else - { - csOp.opcode = '='; - csOp.chars = optEnd - start; - doCsOp(); - } - - return assem.toString(); -}; - -Changeset.inverse = function(cs, lines, alines, pool) -{ - // lines and alines are what the changeset is meant to apply to. - // They may be arrays or objects with .get(i) and .length methods. - // They include final newlines on lines. - - - function lines_get(idx) - { - if (lines.get) - { - return lines.get(idx); - } - else - { - return lines[idx]; - } - } - - function lines_length() - { - if ((typeof lines.length) == "number") - { - return lines.length; - } - else - { - return lines.length(); - } - } - - function alines_get(idx) - { - if (alines.get) - { - return alines.get(idx); - } - else - { - return alines[idx]; - } - } - - function alines_length() - { - if ((typeof alines.length) == "number") - { - return alines.length; - } - else - { - return alines.length(); - } - } - - var curLine = 0; - var curChar = 0; - var curLineOpIter = null; - var curLineOpIterLine; - var curLineNextOp = Changeset.newOp('+'); - - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var builder = Changeset.builder(unpacked.newLen); - - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) - { - - if ((!curLineOpIter) || (curLineOpIterLine != curLine)) - { - // create curLineOpIter and advance it to curChar - curLineOpIter = Changeset.opIterator(alines_get(curLine)); - curLineOpIterLine = curLine; - var indexIntoLine = 0; - var done = false; - while (!done) - { - curLineOpIter.next(curLineNextOp); - if (indexIntoLine + curLineNextOp.chars >= curChar) - { - curLineNextOp.chars -= (curChar - indexIntoLine); - done = true; - } - else - { - indexIntoLine += curLineNextOp.chars; - } - } - } - - while (numChars > 0) - { - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) - { - curLine++; - curChar = 0; - curLineOpIterLine = curLine; - curLineNextOp.chars = 0; - curLineOpIter = Changeset.opIterator(alines_get(curLine)); - } - if (!curLineNextOp.chars) - { - curLineOpIter.next(curLineNextOp); - } - var charsToUse = Math.min(numChars, curLineNextOp.chars); - func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); - numChars -= charsToUse; - curLineNextOp.chars -= charsToUse; - curChar += charsToUse; - } - - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) - { - curLine++; - curChar = 0; - } - } - - function skip(N, L) - { - if (L) - { - curLine += L; - curChar = 0; - } - else - { - if (curLineOpIter && curLineOpIterLine == curLine) - { - consumeAttribRuns(N, function() - {}); - } - else - { - curChar += N; - } - } - } - - function nextText(numChars) - { - var len = 0; - var assem = Changeset.stringAssembler(); - var firstString = lines_get(curLine).substring(curChar); - len += firstString.length; - assem.append(firstString); - - var lineNum = curLine + 1; - while (len < numChars) - { - var nextString = lines_get(lineNum); - len += nextString.length; - assem.append(nextString); - lineNum++; - } - - return assem.toString().substring(0, numChars); - } - - function cachedStrFunc(func) - { - var cache = {}; - return function(s) - { - if (!cache[s]) - { - cache[s] = func(s); - } - return cache[s]; - }; - } - - var attribKeys = []; - var attribValues = []; - while (csIter.hasNext()) - { - var csOp = csIter.next(); - if (csOp.opcode == '=') - { - if (csOp.attribs) - { - attribKeys.length = 0; - attribValues.length = 0; - Changeset.eachAttribNumber(csOp.attribs, function(n) - { - attribKeys.push(pool.getAttribKey(n)); - attribValues.push(pool.getAttribValue(n)); - }); - var undoBackToAttribs = cachedStrFunc(function(attribs) - { - var backAttribs = []; - for (var i = 0; i < attribKeys.length; i++) - { - var appliedKey = attribKeys[i]; - var appliedValue = attribValues[i]; - var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, pool); - if (appliedValue != oldValue) - { - backAttribs.push([appliedKey, oldValue]); - } - } - return Changeset.makeAttribsString('=', backAttribs, pool); - }); - consumeAttribRuns(csOp.chars, function(len, attribs, endsLine) - { - builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); - }); - } - else - { - skip(csOp.chars, csOp.lines); - builder.keep(csOp.chars, csOp.lines); - } - } - else if (csOp.opcode == '+') - { - builder.remove(csOp.chars, csOp.lines); - } - else if (csOp.opcode == '-') - { - var textBank = nextText(csOp.chars); - var textBankIndex = 0; - consumeAttribRuns(csOp.chars, function(len, attribs, endsLine) - { - builder.insert(textBank.substr(textBankIndex, len), attribs); - textBankIndex += len; - }); - } - } - - return Changeset.checkRep(builder.toString()); -}; - -// %CLIENT FILE ENDS HERE% -Changeset.follow = function(cs1, cs2, reverseInsertOrder, pool) -{ - var unpacked1 = Changeset.unpack(cs1); - var unpacked2 = Changeset.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked2.oldLen; - Changeset.assert(len1 == len2, "mismatched follow"); - var chars1 = Changeset.stringIterator(unpacked1.charBank); - var chars2 = Changeset.stringIterator(unpacked2.charBank); - - var oldLen = unpacked1.newLen; - var oldPos = 0; - var newLen = 0; - - var hasInsertFirst = Changeset.attributeTester(['insertorder', 'first'], pool); - - var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) - { - if (op1.opcode == '+' || op2.opcode == '+') - { - var whichToDo; - if (op2.opcode != '+') - { - whichToDo = 1; - } - else if (op1.opcode != '+') - { - whichToDo = 2; - } - else - { - // both + - var firstChar1 = chars1.peek(1); - var firstChar2 = chars2.peek(1); - var insertFirst1 = hasInsertFirst(op1.attribs); - var insertFirst2 = hasInsertFirst(op2.attribs); - if (insertFirst1 && !insertFirst2) - { - whichToDo = 1; - } - else if (insertFirst2 && !insertFirst1) - { - whichToDo = 2; - } - // insert string that doesn't start with a newline first so as not to break up lines - else if (firstChar1 == '\n' && firstChar2 != '\n') - { - whichToDo = 2; - } - else if (firstChar1 != '\n' && firstChar2 == '\n') - { - whichToDo = 1; - } - // break symmetry: - else if (reverseInsertOrder) - { - whichToDo = 2; - } - else - { - whichToDo = 1; - } - } - if (whichToDo == 1) - { - chars1.skip(op1.chars); - opOut.opcode = '='; - opOut.lines = op1.lines; - opOut.chars = op1.chars; - opOut.attribs = ''; - op1.opcode = ''; - } - else - { - // whichToDo == 2 - chars2.skip(op2.chars); - Changeset.copyOp(op2, opOut); - op2.opcode = ''; - } - } - else if (op1.opcode == '-') - { - if (!op2.opcode) - { - op1.opcode = ''; - } - else - { - if (op1.chars <= op2.chars) - { - op2.chars -= op1.chars; - op2.lines -= op1.lines; - op1.opcode = ''; - if (!op2.chars) - { - op2.opcode = ''; - } - } - else - { - op1.chars -= op2.chars; - op1.lines -= op2.lines; - op2.opcode = ''; - } - } - } - else if (op2.opcode == '-') - { - Changeset.copyOp(op2, opOut); - if (!op1.opcode) - { - op2.opcode = ''; - } - else if (op2.chars <= op1.chars) - { - // delete part or all of a keep - op1.chars -= op2.chars; - op1.lines -= op2.lines; - op2.opcode = ''; - if (!op1.chars) - { - op1.opcode = ''; - } - } - else - { - // delete all of a keep, and keep going - opOut.lines = op1.lines; - opOut.chars = op1.chars; - op2.lines -= op1.lines; - op2.chars -= op1.chars; - op1.opcode = ''; - } - } - else if (!op1.opcode) - { - Changeset.copyOp(op2, opOut); - op2.opcode = ''; - } - else if (!op2.opcode) - { - Changeset.copyOp(op1, opOut); - op1.opcode = ''; - } - else - { - // both keeps - opOut.opcode = '='; - opOut.attribs = Changeset.followAttributes(op1.attribs, op2.attribs, pool); - if (op1.chars <= op2.chars) - { - opOut.chars = op1.chars; - opOut.lines = op1.lines; - op2.chars -= op1.chars; - op2.lines -= op1.lines; - op1.opcode = ''; - if (!op2.chars) - { - op2.opcode = ''; - } - } - else - { - opOut.chars = op2.chars; - opOut.lines = op2.lines; - op1.chars -= op2.chars; - op1.lines -= op2.lines; - op2.opcode = ''; - } - } - switch (opOut.opcode) - { - case '=': - oldPos += opOut.chars; - newLen += opOut.chars; - break; - case '-': - oldPos += opOut.chars; - break; - case '+': - newLen += opOut.chars; - break; - } - }); - newLen += oldLen - oldPos; - - return Changeset.pack(oldLen, newLen, newOps, unpacked2.charBank); -}; - -Changeset.followAttributes = function(att1, att2, pool) -{ - // The merge of two sets of attribute changes to the same text - // takes the lexically-earlier value if there are two values - // for the same key. Otherwise, all key/value changes from - // both attribute sets are taken. This operation is the "follow", - // so a set of changes is produced that can be applied to att1 - // to produce the merged set. - if ((!att2) || (!pool)) return ''; - if (!att1) return att2; - var atts = []; - att2.replace(/\*([0-9a-z]+)/g, function(_, a) - { - atts.push(pool.getAttrib(Changeset.parseNum(a))); - return ''; - }); - att1.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var pair1 = pool.getAttrib(Changeset.parseNum(a)); - for (var i = 0; i < atts.length; i++) - { - var pair2 = atts[i]; - if (pair1[0] == pair2[0]) - { - if (pair1[1] <= pair2[1]) - { - // winner of merge is pair1, delete this attribute - atts.splice(i, 1); - } - break; - } - } - return ''; - }); - // we've only removed attributes, so they're already sorted - var buf = Changeset.stringAssembler(); - for (var i = 0; i < atts.length; i++) - { - buf.append('*'); - buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); - } - return buf.toString(); -}; - -exports.Changeset = Changeset; -exports.AttribPool = AttribPool; +exports.Changeset = require('/Changeset'); +exports.AttribPool = require('/AttributePoolFactory').createAttributePool; diff --git a/static/js/easysync2_client.js b/static/js/easysync2_client.js index f4f3d08f..00a864a2 100644 --- a/static/js/easysync2_client.js +++ b/static/js/easysync2_client.js @@ -22,2253 +22,6 @@ * limitations under the License. */ //var _opt = (this.Easysync2Support || null); -var _opt = null; // disable optimization for now -function AttribPool() -{ - var p = {}; - p.numToAttrib = {}; // e.g. {0: ['foo','bar']} - p.attribToNum = {}; // e.g. {'foo,bar': 0} - p.nextNum = 0; - - p.putAttrib = function(attrib, dontAddIfAbsent) - { - var str = String(attrib); - if (str in p.attribToNum) - { - return p.attribToNum[str]; - } - if (dontAddIfAbsent) - { - return -1; - } - var num = p.nextNum++; - p.attribToNum[str] = num; - p.numToAttrib[num] = [String(attrib[0] || ''), String(attrib[1] || '')]; - return num; - }; - - p.getAttrib = function(num) - { - var pair = p.numToAttrib[num]; - if (!pair) return pair; - return [pair[0], pair[1]]; // return a mutable copy - }; - - p.getAttribKey = function(num) - { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[0]; - }; - - p.getAttribValue = function(num) - { - var pair = p.numToAttrib[num]; - if (!pair) return ''; - return pair[1]; - }; - - p.eachAttrib = function(func) - { - for (var n in p.numToAttrib) - { - var pair = p.numToAttrib[n]; - func(pair[0], pair[1]); - } - }; - - p.toJsonable = function() - { - return { - numToAttrib: p.numToAttrib, - nextNum: p.nextNum - }; - }; - - p.fromJsonable = function(obj) - { - p.numToAttrib = obj.numToAttrib; - p.nextNum = obj.nextNum; - p.attribToNum = {}; - for (var n in p.numToAttrib) - { - p.attribToNum[String(p.numToAttrib[n])] = Number(n); - } - return p; - }; - - return p; -} - -var Changeset = {}; - -Changeset.error = function error(msg) -{ - var e = new Error(msg); - e.easysync = true; - throw e; -}; -Changeset.assert = function assert(b, msgParts) -{ - if (!b) - { - var msg = Array.prototype.slice.call(arguments, 1).join(''); - Changeset.error("Changeset: " + msg); - } -}; - -Changeset.parseNum = function(str) -{ - return parseInt(str, 36); -}; -Changeset.numToString = function(num) -{ - return num.toString(36).toLowerCase(); -}; -Changeset.toBaseTen = function(cs) -{ - var dollarIndex = cs.indexOf('$'); - var beforeDollar = cs.substring(0, dollarIndex); - var fromDollar = cs.substring(dollarIndex); - return beforeDollar.replace(/[0-9a-z]+/g, function(s) - { - return String(Changeset.parseNum(s)); - }) + fromDollar; -}; - -Changeset.oldLen = function(cs) -{ - return Changeset.unpack(cs).oldLen; -}; -Changeset.newLen = function(cs) -{ - return Changeset.unpack(cs).newLen; -}; - -Changeset.opIterator = function(opsStr, optStartIndex) -{ - //print(opsStr); - var regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|\?|/g; - var startIndex = (optStartIndex || 0); - var curIndex = startIndex; - var prevIndex = curIndex; - - function nextRegexMatch() - { - prevIndex = curIndex; - var result; - if (_opt) - { - result = _opt.nextOpInString(opsStr, curIndex); - if (result) - { - if (result.opcode() == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - curIndex = result.lastIndex(); - } - } - else - { - regex.lastIndex = curIndex; - result = regex.exec(opsStr); - curIndex = regex.lastIndex; - if (result[0] == '?') - { - Changeset.error("Hit error opcode in op stream"); - } - } - return result; - } - var regexResult = nextRegexMatch(); - var obj = Changeset.newOp(); - - function next(optObj) - { - var op = (optObj || obj); - if (_opt && regexResult) - { - op.attribs = regexResult.attribs(); - op.lines = regexResult.lines(); - op.chars = regexResult.chars(); - op.opcode = regexResult.opcode(); - regexResult = nextRegexMatch(); - } - else if ((!_opt) && regexResult[0]) - { - op.attribs = regexResult[1]; - op.lines = Changeset.parseNum(regexResult[2] || 0); - op.opcode = regexResult[3]; - op.chars = Changeset.parseNum(regexResult[4]); - regexResult = nextRegexMatch(); - } - else - { - Changeset.clearOp(op); - } - return op; - } - - function hasNext() - { - return !!(_opt ? regexResult : regexResult[0]); - } - - function lastIndex() - { - return prevIndex; - } - return { - next: next, - hasNext: hasNext, - lastIndex: lastIndex - }; -}; - -Changeset.clearOp = function(op) -{ - op.opcode = ''; - op.chars = 0; - op.lines = 0; - op.attribs = ''; -}; -Changeset.newOp = function(optOpcode) -{ - return { - opcode: (optOpcode || ''), - chars: 0, - lines: 0, - attribs: '' - }; -}; -Changeset.cloneOp = function(op) -{ - return { - opcode: op.opcode, - chars: op.chars, - lines: op.lines, - attribs: op.attribs - }; -}; -Changeset.copyOp = function(op1, op2) -{ - op2.opcode = op1.opcode; - op2.chars = op1.chars; - op2.lines = op1.lines; - op2.attribs = op1.attribs; -}; -Changeset.opString = function(op) -{ - // just for debugging - if (!op.opcode) return 'null'; - var assem = Changeset.opAssembler(); - assem.append(op); - return assem.toString(); -}; -Changeset.stringOp = function(str) -{ - // just for debugging - return Changeset.opIterator(str).next(); -}; - -Changeset.checkRep = function(cs) -{ - // doesn't check things that require access to attrib pool (e.g. attribute order) - // or original string (e.g. newline positions) - var unpacked = Changeset.unpack(cs); - var oldLen = unpacked.oldLen; - var newLen = unpacked.newLen; - var ops = unpacked.ops; - var charBank = unpacked.charBank; - - var assem = Changeset.smartOpAssembler(); - var oldPos = 0; - var calcNewLen = 0; - var numInserted = 0; - var iter = Changeset.opIterator(ops); - while (iter.hasNext()) - { - var o = iter.next(); - switch (o.opcode) - { - case '=': - oldPos += o.chars; - calcNewLen += o.chars; - break; - case '-': - oldPos += o.chars; - Changeset.assert(oldPos < oldLen, oldPos, " >= ", oldLen, " in ", cs); - break; - case '+': - { - calcNewLen += o.chars; - numInserted += o.chars; - Changeset.assert(calcNewLen < newLen, calcNewLen, " >= ", newLen, " in ", cs); - break; - } - } - assem.append(o); - } - - calcNewLen += oldLen - oldPos; - charBank = charBank.substring(0, numInserted); - while (charBank.length < numInserted) - { - charBank += "?"; - } - - assem.endDocument(); - var normalized = Changeset.pack(oldLen, calcNewLen, assem.toString(), charBank); - Changeset.assert(normalized == cs, normalized, ' != ', cs); - - return cs; -} - -Changeset.smartOpAssembler = function() -{ - // Like opAssembler but able to produce conforming changesets - // from slightly looser input, at the cost of speed. - // Specifically: - // - merges consecutive operations that can be merged - // - strips final "=" - // - ignores 0-length changes - // - reorders consecutive + and - (which margingOpAssembler doesn't do) - var minusAssem = Changeset.mergingOpAssembler(); - var plusAssem = Changeset.mergingOpAssembler(); - var keepAssem = Changeset.mergingOpAssembler(); - var assem = Changeset.stringAssembler(); - var lastOpcode = ''; - var lengthChange = 0; - - function flushKeeps() - { - assem.append(keepAssem.toString()); - keepAssem.clear(); - } - - function flushPlusMinus() - { - assem.append(minusAssem.toString()); - minusAssem.clear(); - assem.append(plusAssem.toString()); - plusAssem.clear(); - } - - function append(op) - { - if (!op.opcode) return; - if (!op.chars) return; - - if (op.opcode == '-') - { - if (lastOpcode == '=') - { - flushKeeps(); - } - minusAssem.append(op); - lengthChange -= op.chars; - } - else if (op.opcode == '+') - { - if (lastOpcode == '=') - { - flushKeeps(); - } - plusAssem.append(op); - lengthChange += op.chars; - } - else if (op.opcode == '=') - { - if (lastOpcode != '=') - { - flushPlusMinus(); - } - keepAssem.append(op); - } - lastOpcode = op.opcode; - } - - function appendOpWithText(opcode, text, attribs, pool) - { - var op = Changeset.newOp(opcode); - op.attribs = Changeset.makeAttribsString(opcode, attribs, pool); - var lastNewlinePos = text.lastIndexOf('\n'); - if (lastNewlinePos < 0) - { - op.chars = text.length; - op.lines = 0; - append(op); - } - else - { - op.chars = lastNewlinePos + 1; - op.lines = text.match(/\n/g).length; - append(op); - op.chars = text.length - (lastNewlinePos + 1); - op.lines = 0; - append(op); - } - } - - function toString() - { - flushPlusMinus(); - flushKeeps(); - return assem.toString(); - } - - function clear() - { - minusAssem.clear(); - plusAssem.clear(); - keepAssem.clear(); - assem.clear(); - lengthChange = 0; - } - - function endDocument() - { - keepAssem.endDocument(); - } - - function getLengthChange() - { - return lengthChange; - } - - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument, - appendOpWithText: appendOpWithText, - getLengthChange: getLengthChange - }; -}; - -if (_opt) -{ - Changeset.mergingOpAssembler = function() - { - var assem = _opt.mergingOpAssembler(); - - function append(op) - { - assem.append(op.opcode, op.chars, op.lines, op.attribs); - } - - function toString() - { - return assem.toString(); - } - - function clear() - { - assem.clear(); - } - - function endDocument() - { - assem.endDocument(); - } - - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument - }; - }; -} -else -{ - Changeset.mergingOpAssembler = function() - { - // This assembler can be used in production; it efficiently - // merges consecutive operations that are mergeable, ignores - // no-ops, and drops final pure "keeps". It does not re-order - // operations. - var assem = Changeset.opAssembler(); - var bufOp = Changeset.newOp(); - - // If we get, for example, insertions [xxx\n,yyy], those don't merge, - // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n]. - // This variable stores the length of yyy and any other newline-less - // ops immediately after it. - var bufOpAdditionalCharsAfterNewline = 0; - - function flush(isEndDocument) - { - if (bufOp.opcode) - { - if (isEndDocument && bufOp.opcode == '=' && !bufOp.attribs) - { - // final merged keep, leave it implicit - } - else - { - assem.append(bufOp); - if (bufOpAdditionalCharsAfterNewline) - { - bufOp.chars = bufOpAdditionalCharsAfterNewline; - bufOp.lines = 0; - assem.append(bufOp); - bufOpAdditionalCharsAfterNewline = 0; - } - } - bufOp.opcode = ''; - } - } - - function append(op) - { - if (op.chars > 0) - { - if (bufOp.opcode == op.opcode && bufOp.attribs == op.attribs) - { - if (op.lines > 0) - { - // bufOp and additional chars are all mergeable into a multi-line op - bufOp.chars += bufOpAdditionalCharsAfterNewline + op.chars; - bufOp.lines += op.lines; - bufOpAdditionalCharsAfterNewline = 0; - } - else if (bufOp.lines == 0) - { - // both bufOp and op are in-line - bufOp.chars += op.chars; - } - else - { - // append in-line text to multi-line bufOp - bufOpAdditionalCharsAfterNewline += op.chars; - } - } - else - { - flush(); - Changeset.copyOp(op, bufOp); - } - } - } - - function endDocument() - { - flush(true); - } - - function toString() - { - flush(); - return assem.toString(); - } - - function clear() - { - assem.clear(); - Changeset.clearOp(bufOp); - } - return { - append: append, - toString: toString, - clear: clear, - endDocument: endDocument - }; - }; -} - -if (_opt) -{ - Changeset.opAssembler = function() - { - var assem = _opt.opAssembler(); - // this function allows op to be mutated later (doesn't keep a ref) - - function append(op) - { - assem.append(op.opcode, op.chars, op.lines, op.attribs); - } - - function toString() - { - return assem.toString(); - } - - function clear() - { - assem.clear(); - } - return { - append: append, - toString: toString, - clear: clear - }; - }; -} -else -{ - Changeset.opAssembler = function() - { - var pieces = []; - // this function allows op to be mutated later (doesn't keep a ref) - - function append(op) - { - pieces.push(op.attribs); - if (op.lines) - { - pieces.push('|', Changeset.numToString(op.lines)); - } - pieces.push(op.opcode); - pieces.push(Changeset.numToString(op.chars)); - } - - function toString() - { - return pieces.join(''); - } - - function clear() - { - pieces.length = 0; - } - return { - append: append, - toString: toString, - clear: clear - }; - }; -} - -Changeset.stringIterator = function(str) -{ - var curIndex = 0; - - function assertRemaining(n) - { - Changeset.assert(n <= remaining(), "!(", n, " <= ", remaining(), ")"); - } - - function take(n) - { - assertRemaining(n); - var s = str.substr(curIndex, n); - curIndex += n; - return s; - } - - function peek(n) - { - assertRemaining(n); - var s = str.substr(curIndex, n); - return s; - } - - function skip(n) - { - assertRemaining(n); - curIndex += n; - } - - function remaining() - { - return str.length - curIndex; - } - return { - take: take, - skip: skip, - remaining: remaining, - peek: peek - }; -}; - -Changeset.stringAssembler = function() -{ - var pieces = []; - - function append(x) - { - pieces.push(String(x)); - } - - function toString() - { - return pieces.join(''); - } - return { - append: append, - toString: toString - }; -}; - -// "lines" need not be an array as long as it supports certain calls (lines_foo inside). -Changeset.textLinesMutator = function(lines) -{ - // Mutates lines, an array of strings, in place. - // Mutation operations have the same constraints as changeset operations - // with respect to newlines, but not the other additional constraints - // (i.e. ins/del ordering, forbidden no-ops, non-mergeability, final newline). - // Can be used to mutate lists of strings where the last char of each string - // is not actually a newline, but for the purposes of N and L values, - // the caller should pretend it is, and for things to work right in that case, the input - // to insert() should be a single line with no newlines. - var curSplice = [0, 0]; - var inSplice = false; - // position in document after curSplice is applied: - var curLine = 0, - curCol = 0; - // invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) && - // curLine >= curSplice[0] - // invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then - // curCol == 0 - - function lines_applySplice(s) - { - lines.splice.apply(lines, s); - } - - function lines_toSource() - { - return lines.toSource(); - } - - function lines_get(idx) - { - if (lines.get) - { - return lines.get(idx); - } - else - { - return lines[idx]; - } - } - // can be unimplemented if removeLines's return value not needed - - function lines_slice(start, end) - { - if (lines.slice) - { - return lines.slice(start, end); - } - else - { - return []; - } - } - - function lines_length() - { - if ((typeof lines.length) == "number") - { - return lines.length; - } - else - { - return lines.length(); - } - } - - function enterSplice() - { - curSplice[0] = curLine; - curSplice[1] = 0; - if (curCol > 0) - { - putCurLineInSplice(); - } - inSplice = true; - } - - function leaveSplice() - { - lines_applySplice(curSplice); - curSplice.length = 2; - curSplice[0] = curSplice[1] = 0; - inSplice = false; - } - - function isCurLineInSplice() - { - return (curLine - curSplice[0] < (curSplice.length - 2)); - } - - function debugPrint(typ) - { - print(typ + ": " + curSplice.toSource() + " / " + curLine + "," + curCol + " / " + lines_toSource()); - } - - function putCurLineInSplice() - { - if (!isCurLineInSplice()) - { - curSplice.push(lines_get(curSplice[0] + curSplice[1])); - curSplice[1]++; - } - return 2 + curLine - curSplice[0]; - } - - function skipLines(L, includeInSplice) - { - if (L) - { - if (includeInSplice) - { - if (!inSplice) - { - enterSplice(); - } - for (var i = 0; i < L; i++) - { - curCol = 0; - putCurLineInSplice(); - curLine++; - } - } - else - { - if (inSplice) - { - if (L > 1) - { - leaveSplice(); - } - else - { - putCurLineInSplice(); - } - } - curLine += L; - curCol = 0; - } - //print(inSplice+" / "+isCurLineInSplice()+" / "+curSplice[0]+" / "+curSplice[1]+" / "+lines.length); -/*if (inSplice && (! isCurLineInSplice()) && (curSplice[0] + curSplice[1] < lines.length)) { - print("BLAH"); - putCurLineInSplice(); - }*/ - // tests case foo in remove(), which isn't otherwise covered in current impl - } - //debugPrint("skip"); - } - - function skip(N, L, includeInSplice) - { - if (N) - { - if (L) - { - skipLines(L, includeInSplice); - } - else - { - if (includeInSplice && !inSplice) - { - enterSplice(); - } - if (inSplice) - { - putCurLineInSplice(); - } - curCol += N; - //debugPrint("skip"); - } - } - } - - function removeLines(L) - { - var removed = ''; - if (L) - { - if (!inSplice) - { - enterSplice(); - } - - function nextKLinesText(k) - { - var m = curSplice[0] + curSplice[1]; - return lines_slice(m, m + k).join(''); - } - if (isCurLineInSplice()) - { - //print(curCol); - if (curCol == 0) - { - removed = curSplice[curSplice.length - 1]; - // print("FOO"); // case foo - curSplice.length--; - removed += nextKLinesText(L - 1); - curSplice[1] += L - 1; - } - else - { - removed = nextKLinesText(L - 1); - curSplice[1] += L - 1; - var sline = curSplice.length - 1; - removed = curSplice[sline].substring(curCol) + removed; - curSplice[sline] = curSplice[sline].substring(0, curCol) + lines_get(curSplice[0] + curSplice[1]); - curSplice[1] += 1; - } - } - else - { - removed = nextKLinesText(L); - curSplice[1] += L; - } - //debugPrint("remove"); - } - return removed; - } - - function remove(N, L) - { - var removed = ''; - if (N) - { - if (L) - { - return removeLines(L); - } - else - { - if (!inSplice) - { - enterSplice(); - } - var sline = putCurLineInSplice(); - removed = curSplice[sline].substring(curCol, curCol + N); - curSplice[sline] = curSplice[sline].substring(0, curCol) + curSplice[sline].substring(curCol + N); - //debugPrint("remove"); - } - } - return removed; - } - - function insert(text, L) - { - if (text) - { - if (!inSplice) - { - enterSplice(); - } - if (L) - { - var newLines = Changeset.splitTextLines(text); - if (isCurLineInSplice()) - { - //if (curCol == 0) { - //curSplice.length--; - //curSplice[1]--; - //Array.prototype.push.apply(curSplice, newLines); - //curLine += newLines.length; - //} - //else { - var sline = curSplice.length - 1; - var theLine = curSplice[sline]; - var lineCol = curCol; - curSplice[sline] = theLine.substring(0, lineCol) + newLines[0]; - curLine++; - newLines.splice(0, 1); - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - curSplice.push(theLine.substring(lineCol)); - curCol = 0; - //} - } - else - { - Array.prototype.push.apply(curSplice, newLines); - curLine += newLines.length; - } - } - else - { - var sline = putCurLineInSplice(); - curSplice[sline] = curSplice[sline].substring(0, curCol) + text + curSplice[sline].substring(curCol); - curCol += text.length; - } - //debugPrint("insert"); - } - } - - function hasMore() - { - //print(lines.length+" / "+inSplice+" / "+(curSplice.length - 2)+" / "+curSplice[1]); - var docLines = lines_length(); - if (inSplice) - { - docLines += curSplice.length - 2 - curSplice[1]; - } - return curLine < docLines; - } - - function close() - { - if (inSplice) - { - leaveSplice(); - } - //debugPrint("close"); - } - - var self = { - skip: skip, - remove: remove, - insert: insert, - close: close, - hasMore: hasMore, - removeLines: removeLines, - skipLines: skipLines - }; - return self; -}; - -Changeset.applyZip = function(in1, idx1, in2, idx2, func) -{ - var iter1 = Changeset.opIterator(in1, idx1); - var iter2 = Changeset.opIterator(in2, idx2); - var assem = Changeset.smartOpAssembler(); - var op1 = Changeset.newOp(); - var op2 = Changeset.newOp(); - var opOut = Changeset.newOp(); - while (op1.opcode || iter1.hasNext() || op2.opcode || iter2.hasNext()) - { - if ((!op1.opcode) && iter1.hasNext()) iter1.next(op1); - if ((!op2.opcode) && iter2.hasNext()) iter2.next(op2); - func(op1, op2, opOut); - if (opOut.opcode) - { - //print(opOut.toSource()); - assem.append(opOut); - opOut.opcode = ''; - } - } - assem.endDocument(); - return assem.toString(); -}; - -Changeset.unpack = function(cs) -{ - var headerRegex = /Z:([0-9a-z]+)([><])([0-9a-z]+)|/; - var headerMatch = headerRegex.exec(cs); - if ((!headerMatch) || (!headerMatch[0])) - { - Changeset.error("Not a changeset: " + cs); - } - var oldLen = Changeset.parseNum(headerMatch[1]); - var changeSign = (headerMatch[2] == '>') ? 1 : -1; - var changeMag = Changeset.parseNum(headerMatch[3]); - var newLen = oldLen + changeSign * changeMag; - var opsStart = headerMatch[0].length; - var opsEnd = cs.indexOf("$"); - if (opsEnd < 0) opsEnd = cs.length; - return { - oldLen: oldLen, - newLen: newLen, - ops: cs.substring(opsStart, opsEnd), - charBank: cs.substring(opsEnd + 1) - }; -}; - -Changeset.pack = function(oldLen, newLen, opsStr, bank) -{ - var lenDiff = newLen - oldLen; - var lenDiffStr = (lenDiff >= 0 ? '>' + Changeset.numToString(lenDiff) : '<' + Changeset.numToString(-lenDiff)); - var a = []; - a.push('Z:', Changeset.numToString(oldLen), lenDiffStr, opsStr, '$', bank); - return a.join(''); -}; - -Changeset.applyToText = function(cs, str) -{ - var unpacked = Changeset.unpack(cs); - Changeset.assert(str.length == unpacked.oldLen, "mismatched apply: ", str.length, " / ", unpacked.oldLen); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var strIter = Changeset.stringIterator(str); - var assem = Changeset.stringAssembler(); - while (csIter.hasNext()) - { - var op = csIter.next(); - switch (op.opcode) - { - case '+': - assem.append(bankIter.take(op.chars)); - break; - case '-': - strIter.skip(op.chars); - break; - case '=': - assem.append(strIter.take(op.chars)); - break; - } - } - assem.append(strIter.take(strIter.remaining())); - return assem.toString(); -}; - -Changeset.mutateTextLines = function(cs, lines) -{ - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var bankIter = Changeset.stringIterator(unpacked.charBank); - var mut = Changeset.textLinesMutator(lines); - while (csIter.hasNext()) - { - var op = csIter.next(); - switch (op.opcode) - { - case '+': - mut.insert(bankIter.take(op.chars), op.lines); - break; - case '-': - mut.remove(op.chars, op.lines); - break; - case '=': - mut.skip(op.chars, op.lines, ( !! op.attribs)); - break; - } - } - mut.close(); -}; - -Changeset.composeAttributes = function(att1, att2, resultIsMutation, pool) -{ - // att1 and att2 are strings like "*3*f*1c", asMutation is a boolean. - // Sometimes attribute (key,value) pairs are treated as attribute presence - // information, while other times they are treated as operations that - // mutate a set of attributes, and this affects whether an empty value - // is a deletion or a change. - // Examples, of the form (att1Items, att2Items, resultIsMutation) -> result - // ([], [(bold, )], true) -> [(bold, )] - // ([], [(bold, )], false) -> [] - // ([], [(bold, true)], true) -> [(bold, true)] - // ([], [(bold, true)], false) -> [(bold, true)] - // ([(bold, true)], [(bold, )], true) -> [(bold, )] - // ([(bold, true)], [(bold, )], false) -> [] - // pool can be null if att2 has no attributes. - if ((!att1) && resultIsMutation) - { - // In the case of a mutation (i.e. composing two changesets), - // an att2 composed with an empy att1 is just att2. If att1 - // is part of an attribution string, then att2 may remove - // attributes that are already gone, so don't do this optimization. - return att2; - } - if (!att2) return att1; - var atts = []; - att1.replace(/\*([0-9a-z]+)/g, function(_, a) - { - atts.push(pool.getAttrib(Changeset.parseNum(a))); - return ''; - }); - att2.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var pair = pool.getAttrib(Changeset.parseNum(a)); - var found = false; - for (var i = 0; i < atts.length; i++) - { - var oldPair = atts[i]; - if (oldPair[0] == pair[0]) - { - if (pair[1] || resultIsMutation) - { - oldPair[1] = pair[1]; - } - else - { - atts.splice(i, 1); - } - found = true; - break; - } - } - if ((!found) && (pair[1] || resultIsMutation)) - { - atts.push(pair); - } - return ''; - }); - atts.sort(); - var buf = Changeset.stringAssembler(); - for (var i = 0; i < atts.length; i++) - { - buf.append('*'); - buf.append(Changeset.numToString(pool.putAttrib(atts[i]))); - } - //print(att1+" / "+att2+" / "+buf.toString()); - return buf.toString(); -}; - -Changeset._slicerZipperFunc = function(attOp, csOp, opOut, pool) -{ - // attOp is the op from the sequence that is being operated on, either an - // attribution string or the earlier of two changesets being composed. - // pool can be null if definitely not needed. - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - if (attOp.opcode == '-') - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - } - else if (!attOp.opcode) - { - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - } - else - { - switch (csOp.opcode) - { - case '-': - { - if (csOp.chars <= attOp.chars) - { - // delete or delete part - if (attOp.opcode == '=') - { - opOut.opcode = '-'; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = ''; - } - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - csOp.opcode = ''; - if (!attOp.chars) - { - attOp.opcode = ''; - } - } - else - { - // delete and keep going - if (attOp.opcode == '=') - { - opOut.opcode = '-'; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = ''; - } - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - attOp.opcode = ''; - } - break; - } - case '+': - { - // insert - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - break; - } - case '=': - { - if (csOp.chars <= attOp.chars) - { - // keep or keep part - opOut.opcode = attOp.opcode; - opOut.chars = csOp.chars; - opOut.lines = csOp.lines; - opOut.attribs = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); - csOp.opcode = ''; - attOp.chars -= csOp.chars; - attOp.lines -= csOp.lines; - if (!attOp.chars) - { - attOp.opcode = ''; - } - } - else - { - // keep and keep going - opOut.opcode = attOp.opcode; - opOut.chars = attOp.chars; - opOut.lines = attOp.lines; - opOut.attribs = Changeset.composeAttributes(attOp.attribs, csOp.attribs, attOp.opcode == '=', pool); - attOp.opcode = ''; - csOp.chars -= attOp.chars; - csOp.lines -= attOp.lines; - } - break; - } - case '': - { - Changeset.copyOp(attOp, opOut); - attOp.opcode = ''; - break; - } - } - } -}; - -Changeset.applyToAttribution = function(cs, astr, pool) -{ - var unpacked = Changeset.unpack(cs); - - return Changeset.applyZip(astr, 0, unpacked.ops, 0, function(op1, op2, opOut) - { - return Changeset._slicerZipperFunc(op1, op2, opOut, pool); - }); -}; - -/*Changeset.oneInsertedLineAtATimeOpIterator = function(opsStr, optStartIndex, charBank) { - var iter = Changeset.opIterator(opsStr, optStartIndex); - var bankIndex = 0; - -};*/ - -Changeset.mutateAttributionLines = function(cs, lines, pool) -{ - //dmesg(cs); - //dmesg(lines.toSource()+" ->"); - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var csBank = unpacked.charBank; - var csBankIndex = 0; - // treat the attribution lines as text lines, mutating a line at a time - var mut = Changeset.textLinesMutator(lines); - - var lineIter = null; - - function isNextMutOp() - { - return (lineIter && lineIter.hasNext()) || mut.hasMore(); - } - - function nextMutOp(destOp) - { - if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) - { - var line = mut.removeLines(1); - lineIter = Changeset.opIterator(line); - } - if (lineIter && lineIter.hasNext()) - { - lineIter.next(destOp); - } - else - { - destOp.opcode = ''; - } - } - var lineAssem = null; - - function outputMutOp(op) - { - //print("outputMutOp: "+op.toSource()); - if (!lineAssem) - { - lineAssem = Changeset.mergingOpAssembler(); - } - lineAssem.append(op); - if (op.lines > 0) - { - Changeset.assert(op.lines == 1, "Can't have op.lines of ", op.lines, " in attribution lines"); - // ship it to the mut - mut.insert(lineAssem.toString(), 1); - lineAssem = null; - } - } - - var csOp = Changeset.newOp(); - var attOp = Changeset.newOp(); - var opOut = Changeset.newOp(); - while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) - { - if ((!csOp.opcode) && csIter.hasNext()) - { - csIter.next(csOp); - } - //print(csOp.toSource()+" "+attOp.toSource()+" "+opOut.toSource()); - //print(csOp.opcode+"/"+csOp.lines+"/"+csOp.attribs+"/"+lineAssem+"/"+lineIter+"/"+(lineIter?lineIter.hasNext():null)); - //print("csOp: "+csOp.toSource()); - if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) - { - break; // done - } - else if (csOp.opcode == '=' && csOp.lines > 0 && (!csOp.attribs) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) - { - // skip multiple lines; this is what makes small changes not order of the document size - mut.skipLines(csOp.lines); - //print("skipped: "+csOp.lines); - csOp.opcode = ''; - } - else if (csOp.opcode == '+') - { - if (csOp.lines > 1) - { - var firstLineLen = csBank.indexOf('\n', csBankIndex) + 1 - csBankIndex; - Changeset.copyOp(csOp, opOut); - csOp.chars -= firstLineLen; - csOp.lines--; - opOut.lines = 1; - opOut.chars = firstLineLen; - } - else - { - Changeset.copyOp(csOp, opOut); - csOp.opcode = ''; - } - outputMutOp(opOut); - csBankIndex += opOut.chars; - opOut.opcode = ''; - } - else - { - if ((!attOp.opcode) && isNextMutOp()) - { - nextMutOp(attOp); - } - //print("attOp: "+attOp.toSource()); - Changeset._slicerZipperFunc(attOp, csOp, opOut, pool); - if (opOut.opcode) - { - outputMutOp(opOut); - opOut.opcode = ''; - } - } - } - - Changeset.assert(!lineAssem, "line assembler not finished"); - mut.close(); - - //dmesg("-> "+lines.toSource()); -}; - -Changeset.joinAttributionLines = function(theAlines) -{ - var assem = Changeset.mergingOpAssembler(); - for (var i = 0; i < theAlines.length; i++) - { - var aline = theAlines[i]; - var iter = Changeset.opIterator(aline); - while (iter.hasNext()) - { - assem.append(iter.next()); - } - } - return assem.toString(); -}; - -Changeset.splitAttributionLines = function(attrOps, text) -{ - var iter = Changeset.opIterator(attrOps); - var assem = Changeset.mergingOpAssembler(); - var lines = []; - var pos = 0; - - function appendOp(op) - { - assem.append(op); - if (op.lines > 0) - { - lines.push(assem.toString()); - assem.clear(); - } - pos += op.chars; - } - - while (iter.hasNext()) - { - var op = iter.next(); - var numChars = op.chars; - var numLines = op.lines; - while (numLines > 1) - { - var newlineEnd = text.indexOf('\n', pos) + 1; - Changeset.assert(newlineEnd > 0, "newlineEnd <= 0 in splitAttributionLines"); - op.chars = newlineEnd - pos; - op.lines = 1; - appendOp(op); - numChars -= op.chars; - numLines -= op.lines; - } - if (numLines == 1) - { - op.chars = numChars; - op.lines = 1; - } - appendOp(op); - } - - return lines; -}; - -Changeset.splitTextLines = function(text) -{ - return text.match(/[^\n]*(?:\n|[^\n]$)/g); -}; - -Changeset.compose = function(cs1, cs2, pool) -{ - var unpacked1 = Changeset.unpack(cs1); - var unpacked2 = Changeset.unpack(cs2); - var len1 = unpacked1.oldLen; - var len2 = unpacked1.newLen; - Changeset.assert(len2 == unpacked2.oldLen, "mismatched composition"); - var len3 = unpacked2.newLen; - var bankIter1 = Changeset.stringIterator(unpacked1.charBank); - var bankIter2 = Changeset.stringIterator(unpacked2.charBank); - var bankAssem = Changeset.stringAssembler(); - - var newOps = Changeset.applyZip(unpacked1.ops, 0, unpacked2.ops, 0, function(op1, op2, opOut) - { - //var debugBuilder = Changeset.stringAssembler(); - //debugBuilder.append(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' / '); - var op1code = op1.opcode; - var op2code = op2.opcode; - if (op1code == '+' && op2code == '-') - { - bankIter1.skip(Math.min(op1.chars, op2.chars)); - } - Changeset._slicerZipperFunc(op1, op2, opOut, pool); - if (opOut.opcode == '+') - { - if (op2code == '+') - { - bankAssem.append(bankIter2.take(opOut.chars)); - } - else - { - bankAssem.append(bankIter1.take(opOut.chars)); - } - } - - //debugBuilder.append(Changeset.opString(op1)); - //debugBuilder.append(','); - //debugBuilder.append(Changeset.opString(op2)); - //debugBuilder.append(' -> '); - //debugBuilder.append(Changeset.opString(opOut)); - //print(debugBuilder.toString()); - }); - - return Changeset.pack(len1, len3, newOps, bankAssem.toString()); -}; - -Changeset.attributeTester = function(attribPair, pool) -{ - // returns a function that tests if a string of attributes - // (e.g. *3*4) contains a given attribute key,value that - // is already present in the pool. - if (!pool) - { - return never; - } - var attribNum = pool.putAttrib(attribPair, true); - if (attribNum < 0) - { - return never; - } - else - { - var re = new RegExp('\\*' + Changeset.numToString(attribNum) + '(?!\\w)'); - return function(attribs) - { - return re.test(attribs); - }; - } - - function never(attribs) - { - return false; - } -}; - -Changeset.identity = function(N) -{ - return Changeset.pack(N, N, "", ""); -}; - -Changeset.makeSplice = function(oldFullText, spliceStart, numRemoved, newText, optNewTextAPairs, pool) -{ - var oldLen = oldFullText.length; - - if (spliceStart >= oldLen) - { - spliceStart = oldLen - 1; - } - if (numRemoved > oldFullText.length - spliceStart - 1) - { - numRemoved = oldFullText.length - spliceStart - 1; - } - var oldText = oldFullText.substring(spliceStart, spliceStart + numRemoved); - var newLen = oldLen + newText.length - oldText.length; - - var assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('=', oldFullText.substring(0, spliceStart)); - assem.appendOpWithText('-', oldText); - assem.appendOpWithText('+', newText, optNewTextAPairs, pool); - assem.endDocument(); - return Changeset.pack(oldLen, newLen, assem.toString(), newText); -}; - -Changeset.toSplices = function(cs) -{ - // get a list of splices, [startChar, endChar, newText] - var unpacked = Changeset.unpack(cs); - var splices = []; - - var oldPos = 0; - var iter = Changeset.opIterator(unpacked.ops); - var charIter = Changeset.stringIterator(unpacked.charBank); - var inSplice = false; - while (iter.hasNext()) - { - var op = iter.next(); - if (op.opcode == '=') - { - oldPos += op.chars; - inSplice = false; - } - else - { - if (!inSplice) - { - splices.push([oldPos, oldPos, ""]); - inSplice = true; - } - if (op.opcode == '-') - { - oldPos += op.chars; - splices[splices.length - 1][1] += op.chars; - } - else if (op.opcode == '+') - { - splices[splices.length - 1][2] += charIter.take(op.chars); - } - } - } - - return splices; -}; - -Changeset.characterRangeFollow = function(cs, startChar, endChar, insertionsAfter) -{ - var newStartChar = startChar; - var newEndChar = endChar; - var splices = Changeset.toSplices(cs); - var lengthChangeSoFar = 0; - for (var i = 0; i < splices.length; i++) - { - var splice = splices[i]; - var spliceStart = splice[0] + lengthChangeSoFar; - var spliceEnd = splice[1] + lengthChangeSoFar; - var newTextLength = splice[2].length; - var thisLengthChange = newTextLength - (spliceEnd - spliceStart); - - if (spliceStart <= newStartChar && spliceEnd >= newEndChar) - { - // splice fully replaces/deletes range - // (also case that handles insertion at a collapsed selection) - if (insertionsAfter) - { - newStartChar = newEndChar = spliceStart; - } - else - { - newStartChar = newEndChar = spliceStart + newTextLength; - } - } - else if (spliceEnd <= newStartChar) - { - // splice is before range - newStartChar += thisLengthChange; - newEndChar += thisLengthChange; - } - else if (spliceStart >= newEndChar) - { - // splice is after range - } - else if (spliceStart >= newStartChar && spliceEnd <= newEndChar) - { - // splice is inside range - newEndChar += thisLengthChange; - } - else if (spliceEnd < newEndChar) - { - // splice overlaps beginning of range - newStartChar = spliceStart + newTextLength; - newEndChar += thisLengthChange; - } - else - { - // splice overlaps end of range - newEndChar = spliceStart; - } - - lengthChangeSoFar += thisLengthChange; - } - - return [newStartChar, newEndChar]; -}; - -Changeset.moveOpsToNewPool = function(cs, oldPool, newPool) -{ - // works on changeset or attribution string - var dollarPos = cs.indexOf('$'); - if (dollarPos < 0) - { - dollarPos = cs.length; - } - var upToDollar = cs.substring(0, dollarPos); - var fromDollar = cs.substring(dollarPos); - // order of attribs stays the same - return upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) - { - var oldNum = Changeset.parseNum(a); - var pair = oldPool.getAttrib(oldNum); - var newNum = newPool.putAttrib(pair); - return '*' + Changeset.numToString(newNum); - }) + fromDollar; -}; - -Changeset.makeAttribution = function(text) -{ - var assem = Changeset.smartOpAssembler(); - assem.appendOpWithText('+', text); - return assem.toString(); -}; - -// callable on a changeset, attribution string, or attribs property of an op -Changeset.eachAttribNumber = function(cs, func) -{ - var dollarPos = cs.indexOf('$'); - if (dollarPos < 0) - { - dollarPos = cs.length; - } - var upToDollar = cs.substring(0, dollarPos); - - upToDollar.replace(/\*([0-9a-z]+)/g, function(_, a) - { - func(Changeset.parseNum(a)); - return ''; - }); -}; - -// callable on a changeset, attribution string, or attribs property of an op, -// though it may easily create adjacent ops that can be merged. -Changeset.filterAttribNumbers = function(cs, filter) -{ - return Changeset.mapAttribNumbers(cs, filter); -}; - -Changeset.mapAttribNumbers = function(cs, func) -{ - var dollarPos = cs.indexOf('$'); - if (dollarPos < 0) - { - dollarPos = cs.length; - } - var upToDollar = cs.substring(0, dollarPos); - - var newUpToDollar = upToDollar.replace(/\*([0-9a-z]+)/g, function(s, a) - { - var n = func(Changeset.parseNum(a)); - if (n === true) - { - return s; - } - else if ((typeof n) === "number") - { - return '*' + Changeset.numToString(n); - } - else - { - return ''; - } - }); - - return newUpToDollar + cs.substring(dollarPos); -}; - -Changeset.makeAText = function(text, attribs) -{ - return { - text: text, - attribs: (attribs || Changeset.makeAttribution(text)) - }; -}; - -Changeset.applyToAText = function(cs, atext, pool) -{ - return { - text: Changeset.applyToText(cs, atext.text), - attribs: Changeset.applyToAttribution(cs, atext.attribs, pool) - }; -}; - -Changeset.cloneAText = function(atext) -{ - return { - text: atext.text, - attribs: atext.attribs - }; -}; - -Changeset.copyAText = function(atext1, atext2) -{ - atext2.text = atext1.text; - atext2.attribs = atext1.attribs; -}; - -Changeset.appendATextToAssembler = function(atext, assem) -{ - // intentionally skips last newline char of atext - var iter = Changeset.opIterator(atext.attribs); - var op = Changeset.newOp(); - while (iter.hasNext()) - { - iter.next(op); - if (!iter.hasNext()) - { - // last op, exclude final newline - if (op.lines <= 1) - { - op.lines = 0; - op.chars--; - if (op.chars) - { - assem.append(op); - } - } - else - { - var nextToLastNewlineEnd = atext.text.lastIndexOf('\n', atext.text.length - 2) + 1; - var lastLineLength = atext.text.length - nextToLastNewlineEnd - 1; - op.lines--; - op.chars -= (lastLineLength + 1); - assem.append(op); - op.lines = 0; - op.chars = lastLineLength; - if (op.chars) - { - assem.append(op); - } - } - } - else - { - assem.append(op); - } - } -}; - -Changeset.prepareForWire = function(cs, pool) -{ - var newPool = new AttribPool(); - var newCs = Changeset.moveOpsToNewPool(cs, pool, newPool); - return { - translated: newCs, - pool: newPool - }; -}; - -Changeset.isIdentity = function(cs) -{ - var unpacked = Changeset.unpack(cs); - return unpacked.ops == "" && unpacked.oldLen == unpacked.newLen; -}; - -Changeset.opAttributeValue = function(op, key, pool) -{ - return Changeset.attribsAttributeValue(op.attribs, key, pool); -}; - -Changeset.attribsAttributeValue = function(attribs, key, pool) -{ - var value = ''; - if (attribs) - { - Changeset.eachAttribNumber(attribs, function(n) - { - if (pool.getAttribKey(n) == key) - { - value = pool.getAttribValue(n); - } - }); - } - return value; -}; - -Changeset.builder = function(oldLen) -{ - var assem = Changeset.smartOpAssembler(); - var o = Changeset.newOp(); - var charBank = Changeset.stringAssembler(); - - var self = { - // attribs are [[key1,value1],[key2,value2],...] or '*0*1...' (no pool needed in latter case) - keep: function(N, L, attribs, pool) - { - o.opcode = '='; - o.attribs = (attribs && Changeset.makeAttribsString('=', attribs, pool)) || ''; - o.chars = N; - o.lines = (L || 0); - assem.append(o); - return self; - }, - keepText: function(text, attribs, pool) - { - assem.appendOpWithText('=', text, attribs, pool); - return self; - }, - insert: function(text, attribs, pool) - { - assem.appendOpWithText('+', text, attribs, pool); - charBank.append(text); - return self; - }, - remove: function(N, L) - { - o.opcode = '-'; - o.attribs = ''; - o.chars = N; - o.lines = (L || 0); - assem.append(o); - return self; - }, - toString: function() - { - assem.endDocument(); - var newLen = oldLen + assem.getLengthChange(); - return Changeset.pack(oldLen, newLen, assem.toString(), charBank.toString()); - } - }; - - return self; -}; - -Changeset.makeAttribsString = function(opcode, attribs, pool) -{ - // makeAttribsString(opcode, '*3') or makeAttribsString(opcode, [['foo','bar']], myPool) work - if (!attribs) - { - return ''; - } - else if ((typeof attribs) == "string") - { - return attribs; - } - else if (pool && attribs && attribs.length) - { - if (attribs.length > 1) - { - attribs = attribs.slice(); - attribs.sort(); - } - var result = []; - for (var i = 0; i < attribs.length; i++) - { - var pair = attribs[i]; - if (opcode == '=' || (opcode == '+' && pair[1])) - { - result.push('*' + Changeset.numToString(pool.putAttrib(pair))); - } - } - return result.join(''); - } -}; - -// like "substring" but on a single-line attribution string -Changeset.subattribution = function(astr, start, optEnd) -{ - var iter = Changeset.opIterator(astr, 0); - var assem = Changeset.smartOpAssembler(); - var attOp = Changeset.newOp(); - var csOp = Changeset.newOp(); - var opOut = Changeset.newOp(); - - function doCsOp() - { - if (csOp.chars) - { - while (csOp.opcode && (attOp.opcode || iter.hasNext())) - { - if (!attOp.opcode) iter.next(attOp); - - if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) - { - csOp.lines++; - } - - Changeset._slicerZipperFunc(attOp, csOp, opOut, null); - if (opOut.opcode) - { - assem.append(opOut); - opOut.opcode = ''; - } - } - } - } - - csOp.opcode = '-'; - csOp.chars = start; - - doCsOp(); - - if (optEnd === undefined) - { - if (attOp.opcode) - { - assem.append(attOp); - } - while (iter.hasNext()) - { - iter.next(attOp); - assem.append(attOp); - } - } - else - { - csOp.opcode = '='; - csOp.chars = optEnd - start; - doCsOp(); - } - - return assem.toString(); -}; - -Changeset.inverse = function(cs, lines, alines, pool) -{ - // lines and alines are what the changeset is meant to apply to. - // They may be arrays or objects with .get(i) and .length methods. - // They include final newlines on lines. - - function lines_get(idx) - { - if (lines.get) - { - return lines.get(idx); - } - else - { - return lines[idx]; - } - } - - function lines_length() - { - if ((typeof lines.length) == "number") - { - return lines.length; - } - else - { - return lines.length(); - } - } - - function alines_get(idx) - { - if (alines.get) - { - return alines.get(idx); - } - else - { - return alines[idx]; - } - } - - function alines_length() - { - if ((typeof alines.length) == "number") - { - return alines.length; - } - else - { - return alines.length(); - } - } - - var curLine = 0; - var curChar = 0; - var curLineOpIter = null; - var curLineOpIterLine; - var curLineNextOp = Changeset.newOp('+'); - - var unpacked = Changeset.unpack(cs); - var csIter = Changeset.opIterator(unpacked.ops); - var builder = Changeset.builder(unpacked.newLen); - - function consumeAttribRuns(numChars, func /*(len, attribs, endsLine)*/ ) - { - - if ((!curLineOpIter) || (curLineOpIterLine != curLine)) - { - // create curLineOpIter and advance it to curChar - curLineOpIter = Changeset.opIterator(alines_get(curLine)); - curLineOpIterLine = curLine; - var indexIntoLine = 0; - var done = false; - while (!done) - { - curLineOpIter.next(curLineNextOp); - if (indexIntoLine + curLineNextOp.chars >= curChar) - { - curLineNextOp.chars -= (curChar - indexIntoLine); - done = true; - } - else - { - indexIntoLine += curLineNextOp.chars; - } - } - } - - while (numChars > 0) - { - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) - { - curLine++; - curChar = 0; - curLineOpIterLine = curLine; - curLineNextOp.chars = 0; - curLineOpIter = Changeset.opIterator(alines_get(curLine)); - } - if (!curLineNextOp.chars) - { - curLineOpIter.next(curLineNextOp); - } - var charsToUse = Math.min(numChars, curLineNextOp.chars); - func(charsToUse, curLineNextOp.attribs, charsToUse == curLineNextOp.chars && curLineNextOp.lines > 0); - numChars -= charsToUse; - curLineNextOp.chars -= charsToUse; - curChar += charsToUse; - } - - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) - { - curLine++; - curChar = 0; - } - } - - function skip(N, L) - { - if (L) - { - curLine += L; - curChar = 0; - } - else - { - if (curLineOpIter && curLineOpIterLine == curLine) - { - consumeAttribRuns(N, function() - {}); - } - else - { - curChar += N; - } - } - } - - function nextText(numChars) - { - var len = 0; - var assem = Changeset.stringAssembler(); - var firstString = lines_get(curLine).substring(curChar); - len += firstString.length; - assem.append(firstString); - - var lineNum = curLine + 1; - while (len < numChars) - { - var nextString = lines_get(lineNum); - len += nextString.length; - assem.append(nextString); - lineNum++; - } - - return assem.toString().substring(0, numChars); - } - - function cachedStrFunc(func) - { - var cache = {}; - return function(s) - { - if (!cache[s]) - { - cache[s] = func(s); - } - return cache[s]; - }; - } - - var attribKeys = []; - var attribValues = []; - while (csIter.hasNext()) - { - var csOp = csIter.next(); - if (csOp.opcode == '=') - { - if (csOp.attribs) - { - attribKeys.length = 0; - attribValues.length = 0; - Changeset.eachAttribNumber(csOp.attribs, function(n) - { - attribKeys.push(pool.getAttribKey(n)); - attribValues.push(pool.getAttribValue(n)); - }); - var undoBackToAttribs = cachedStrFunc(function(attribs) - { - var backAttribs = []; - for (var i = 0; i < attribKeys.length; i++) - { - var appliedKey = attribKeys[i]; - var appliedValue = attribValues[i]; - var oldValue = Changeset.attribsAttributeValue(attribs, appliedKey, pool); - if (appliedValue != oldValue) - { - backAttribs.push([appliedKey, oldValue]); - } - } - return Changeset.makeAttribsString('=', backAttribs, pool); - }); - consumeAttribRuns(csOp.chars, function(len, attribs, endsLine) - { - builder.keep(len, endsLine ? 1 : 0, undoBackToAttribs(attribs)); - }); - } - else - { - skip(csOp.chars, csOp.lines); - builder.keep(csOp.chars, csOp.lines); - } - } - else if (csOp.opcode == '+') - { - builder.remove(csOp.chars, csOp.lines); - } - else if (csOp.opcode == '-') - { - var textBank = nextText(csOp.chars); - var textBankIndex = 0; - consumeAttribRuns(csOp.chars, function(len, attribs, endsLine) - { - builder.insert(textBank.substr(textBankIndex, len), attribs); - textBankIndex += len; - }); - } - } - - return Changeset.checkRep(builder.toString()); -}; - -exports.Changeset = Changeset; -exports.AttribPool = AttribPool; +exports.Changeset = require('/Changeset'); +exports.AttribPool = require('/AttributePoolFactory').createAttributePool;