diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 5d4871c5e..2b0e82482 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -184,11 +184,54 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; * Iterator over a changeset's operations. * * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. - * - * @typedef {object} OpIter - * @property {Function} hasNext - - * @property {Function} next - */ +class OpIter { + /** + * @param {string} ops - String encoding the change operations to iterate over. + */ + constructor(ops) { + this._ops = ops; + this._regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; + this._nextMatch = this._nextRegexMatch(); + } + + _nextRegexMatch() { + const match = this._regex.exec(this._ops); + if (!match) return null; + if (match[5] === '$') return null; // Start of the insert operation character bank. + if (match[5] != null) error(`invalid operation: ${this._ops.slice(this._regex.lastIndex - 1)}`); + return match; + } + + /** + * @returns {boolean} Whether there are any remaining operations. + */ + hasNext() { + return this._nextMatch && !!this._nextMatch[0]; + } + + /** + * Returns the next operation object and advances the iterator. + * + * Note: This does NOT implement the ECMAScript iterator protocol. + * + * @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value. + * @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are + * no more operations. + */ + next(opOut = new Op()) { + if (this.hasNext()) { + opOut.attribs = this._nextMatch[1]; + opOut.lines = exports.parseNum(this._nextMatch[2] || '0'); + opOut.opcode = this._nextMatch[3]; + opOut.chars = exports.parseNum(this._nextMatch[4]); + this._nextMatch = this._nextRegexMatch(); + } else { + clearOp(opOut); + } + return opOut; + } +} /** * Creates an iterator which decodes string changeset operations. @@ -196,38 +239,7 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; * @param {string} opsStr - String encoding of the change operations to perform. * @returns {OpIter} Operator iterator object. */ -exports.opIterator = (opsStr) => { - const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; - - const nextRegexMatch = () => { - const result = regex.exec(opsStr); - if (!result) return null; - if (result[5] === '$') return null; // Start of the insert operation character bank. - if (result[5] != null) error(`invalid operation: ${opsStr.slice(regex.lastIndex - 1)}`); - return result; - }; - let regexResult = nextRegexMatch(); - - const hasNext = () => regexResult && !!regexResult[0]; - - const next = (op = new Op()) => { - if (hasNext()) { - op.attribs = regexResult[1]; - op.lines = exports.parseNum(regexResult[2] || '0'); - op.opcode = regexResult[3]; - op.chars = exports.parseNum(regexResult[4]); - regexResult = nextRegexMatch(); - } else { - clearOp(op); - } - return op; - }; - - return { - next, - hasNext, - }; -}; +exports.opIterator = (opsStr) => new OpIter(opsStr); /** * Cleans an Op object. @@ -352,7 +364,7 @@ exports.checkRep = (cs) => { let oldPos = 0; let calcNewLen = 0; let numInserted = 0; - const iter = exports.opIterator(ops); + const iter = new OpIter(ops); while (iter.hasNext()) { const o = iter.next(); switch (o.opcode) { @@ -1005,8 +1017,8 @@ class TextLinesMutator { * @returns {string} the integrated changeset */ const applyZip = (in1, in2, func) => { - const iter1 = exports.opIterator(in1); - const iter2 = exports.opIterator(in2); + const iter1 = new OpIter(in1); + const iter2 = new OpIter(in2); const assem = exports.smartOpAssembler(); const op1 = new Op(); const op2 = new Op(); @@ -1075,7 +1087,7 @@ exports.pack = (oldLen, newLen, opsStr, bank) => { exports.applyToText = (cs, str) => { const unpacked = exports.unpack(cs); assert(str.length === unpacked.oldLen, `mismatched apply: ${str.length} / ${unpacked.oldLen}`); - const csIter = exports.opIterator(unpacked.ops); + const csIter = new OpIter(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const strIter = exports.stringIterator(str); const assem = exports.stringAssembler(); @@ -1120,7 +1132,7 @@ exports.applyToText = (cs, str) => { */ exports.mutateTextLines = (cs, lines) => { const unpacked = exports.unpack(cs); - const csIter = exports.opIterator(unpacked.ops); + const csIter = new OpIter(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const mut = new TextLinesMutator(lines); while (csIter.hasNext()) { @@ -1251,7 +1263,7 @@ exports.applyToAttribution = (cs, astr, pool) => { exports.mutateAttributionLines = (cs, lines, pool) => { const unpacked = exports.unpack(cs); - const csIter = exports.opIterator(unpacked.ops); + const csIter = new OpIter(unpacked.ops); const csBank = unpacked.charBank; let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time @@ -1265,7 +1277,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { const nextMutOp = () => { if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { const line = mut.removeLines(1); - lineIter = exports.opIterator(line); + lineIter = new OpIter(line); } if (!lineIter || !lineIter.hasNext()) return new Op(); return lineIter.next(); @@ -1328,7 +1340,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { exports.joinAttributionLines = (theAlines) => { const assem = exports.mergingOpAssembler(); for (const aline of theAlines) { - const iter = exports.opIterator(aline); + const iter = new OpIter(aline); while (iter.hasNext()) { assem.append(iter.next()); } @@ -1337,7 +1349,7 @@ exports.joinAttributionLines = (theAlines) => { }; exports.splitAttributionLines = (attrOps, text) => { - const iter = exports.opIterator(attrOps); + const iter = new OpIter(attrOps); const assem = exports.mergingOpAssembler(); const lines = []; let pos = 0; @@ -1495,7 +1507,7 @@ const toSplices = (cs) => { const splices = []; let oldPos = 0; - const iter = exports.opIterator(unpacked.ops); + const iter = new OpIter(unpacked.ops); const charIter = exports.stringIterator(unpacked.charBank); let inSplice = false; while (iter.hasNext()) { @@ -1742,7 +1754,7 @@ exports.copyAText = (atext1, atext2) => { */ exports.opsFromAText = function* (atext) { // intentionally skips last newline char of atext - const iter = exports.opIterator(atext.attribs); + const iter = new OpIter(atext.attribs); let lastOp = null; while (iter.hasNext()) { if (lastOp != null) yield lastOp; @@ -1964,7 +1976,7 @@ exports.makeAttribsString = (opcode, attribs, pool) => { * Like "substring" but on a single-line attribution string. */ exports.subattribution = (astr, start, optEnd) => { - const iter = exports.opIterator(astr); + const iter = new OpIter(astr); const assem = exports.smartOpAssembler(); let attOp = new Op(); const csOp = new Op(); @@ -2033,13 +2045,13 @@ exports.inverse = (cs, lines, alines, pool) => { let curLineNextOp = new Op('+'); const unpacked = exports.unpack(cs); - const csIter = exports.opIterator(unpacked.ops); + const csIter = new OpIter(unpacked.ops); const builder = exports.builder(unpacked.newLen); const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { // create curLineOpIter and advance it to curChar - curLineOpIter = exports.opIterator(alinesGet(curLine)); + curLineOpIter = new OpIter(alinesGet(curLine)); curLineOpIterLine = curLine; let indexIntoLine = 0; while (curLineOpIter.hasNext()) { @@ -2058,7 +2070,7 @@ exports.inverse = (cs, lines, alines, pool) => { curChar = 0; curLineOpIterLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = exports.opIterator(alinesGet(curLine)); + curLineOpIter = new OpIter(alinesGet(curLine)); } if (!curLineNextOp.chars) { curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : new Op();