diff --git a/CHANGELOG.md b/CHANGELOG.md index 03aab889..a16f29c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ * `eachAttribNumber()` * `makeAttribsString()` * `opAttributeValue()` + * `opIterator()`: Deprecated in favor of the new `deserializeOps()` generator + function. * `appendATextToAssembler()`: Deprecated in favor of the new `opsFromAText()` generator function. * `newOp()`: Deprecated in favor of the new `Op` class. diff --git a/doc/api/hooks_server-side.md b/doc/api/hooks_server-side.md index 38cca9ba..aa19adec 100644 --- a/doc/api/hooks_server-side.md +++ b/doc/api/hooks_server-side.md @@ -670,9 +670,8 @@ const Changeset = require('ep_etherpad-lite/static/js/Changeset'); exports.getLineHTMLForExport = async (hookName, context) => { if (!context.attribLine) return; - const opIter = Changeset.opIterator(context.attribLine); - if (!opIter.hasNext()) return; - const op = opIter.next(); + const [op] = Changeset.deserializeOps(context.attribLine); + if (op == null) return; const heading = AttributeMap.fromString(op.attribs, context.apool).get('heading'); if (!heading) return; context.lineContent = `<${heading}>${context.lineContent}`; diff --git a/src/node/db/API.js b/src/node/db/API.js index d4886755..0c3ddae4 100644 --- a/src/node/db/API.js +++ b/src/node/db/API.js @@ -527,12 +527,10 @@ exports.restoreRevision = async (padID, rev) => { atext.text += '\n'; const eachAttribRun = (attribs, func) => { - const attribsIter = Changeset.opIterator(attribs); let textIndex = 0; const newTextStart = 0; const newTextEnd = atext.text.length; - while (attribsIter.hasNext()) { - const op = attribsIter.next(); + for (const op of Changeset.deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); diff --git a/src/node/handler/PadMessageHandler.js b/src/node/handler/PadMessageHandler.js index 868ca5b4..0225b00d 100644 --- a/src/node/handler/PadMessageHandler.js +++ b/src/node/handler/PadMessageHandler.js @@ -586,12 +586,7 @@ const handleUserChanges = async (socket, message) => { Changeset.checkRep(changeset); // Validate all added 'author' attribs to be the same value as the current user - const iterator = Changeset.opIterator(Changeset.unpack(changeset).ops); - let op; - - while (iterator.hasNext()) { - op = iterator.next(); - + for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) { // + can add text with attribs // = can change or add attribs // - can have attribs, but they are discarded and don't show up in the attribs - @@ -741,10 +736,8 @@ const _correctMarkersInPad = (atext, apool) => { // collect char positions of line markers (e.g. bullets) in new atext // that aren't at the start of a line const badMarkers = []; - const iter = Changeset.opIterator(atext.attribs); let offset = 0; - while (iter.hasNext()) { - const op = iter.next(); + for (const op of Changeset.deserializeOps(atext.attribs)) { const attribs = AttributeMap.fromString(op.attribs, apool); const hasMarker = AttributeManager.lineAttributes.some((a) => attribs.has(a)); if (hasMarker) { diff --git a/src/node/utils/ExportHelper.js b/src/node/utils/ExportHelper.js index 401fad70..7962476e 100644 --- a/src/node/utils/ExportHelper.js +++ b/src/node/utils/ExportHelper.js @@ -52,9 +52,8 @@ exports._analyzeLine = (text, aline, apool) => { let lineMarker = 0; line.listLevel = 0; if (aline) { - const opIter = Changeset.opIterator(aline); - if (opIter.hasNext()) { - const op = opIter.next(); + const [op] = Changeset.deserializeOps(aline); + if (op != null) { const attribs = AttributeMap.fromString(op.attribs, apool); let listType = attribs.get('list'); if (listType) { diff --git a/src/node/utils/ExportHtml.js b/src/node/utils/ExportHtml.js index 800798f9..3d5f4cc7 100644 --- a/src/node/utils/ExportHtml.js +++ b/src/node/utils/ExportHtml.js @@ -197,13 +197,12 @@ const getHTMLFromAtext = async (pad, atext, authorColors) => { return; } - const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; // this iterates over every op string and decides which tags to open or to close // based on the attribs used - while (iter.hasNext()) { - const o = iter.next(); + for (const o of ops) { const usedAttribs = []; // mark all attribs as used diff --git a/src/node/utils/ExportTxt.js b/src/node/utils/ExportTxt.js index 1d7ce546..9511dd0e 100644 --- a/src/node/utils/ExportTxt.js +++ b/src/node/utils/ExportTxt.js @@ -76,11 +76,10 @@ const getTXTFromAtext = (pad, atext, authorColors) => { return; } - const iter = Changeset.opIterator(Changeset.subattribution(attribs, idx, idx + numChars)); + const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars)); idx += numChars; - while (iter.hasNext()) { - const o = iter.next(); + for (const o of ops) { let propChanged = false; for (const a of attributes.decodeAttribString(o.attribs)) { diff --git a/src/node/utils/ImportHtml.js b/src/node/utils/ImportHtml.js index 58b79f3a..059d57af 100644 --- a/src/node/utils/ImportHtml.js +++ b/src/node/utils/ImportHtml.js @@ -67,12 +67,10 @@ exports.setPadHTML = async (pad, html) => { const builder = Changeset.builder(1); // assemble each line into the builder - const attribsIter = Changeset.opIterator(newAttribs); let textIndex = 0; const newTextStart = 0; const newTextEnd = newText.length; - while (attribsIter.hasNext()) { - const op = attribsIter.next(); + for (const op of Changeset.deserializeOps(newAttribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { const start = Math.max(newTextStart, textIndex); diff --git a/src/node/utils/padDiff.js b/src/node/utils/padDiff.js index d0c61633..4ab276b4 100644 --- a/src/node/utils/padDiff.js +++ b/src/node/utils/padDiff.js @@ -35,16 +35,10 @@ PadDiff.prototype._isClearAuthorship = function (changeset) { return false; } - // lets iterator over the operators - const iterator = Changeset.opIterator(unpacked.ops); - - // get the first operator, this should be a clear operator - const clearOperator = iterator.next(); + const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops); // check if there is only one operator - if (iterator.hasNext() === true) { - return false; - } + if (anotherOp != null) return false; // check if this operator doesn't change text if (clearOperator.opcode !== '=') { @@ -212,7 +206,6 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { // unpack const unpacked = Changeset.unpack(changeset); - const iterator = Changeset.opIterator(unpacked.ops); const assem = Changeset.opAssembler(); // create deleted attribs @@ -220,10 +213,7 @@ PadDiff.prototype._extendChangesetWithAuthor = (changeset, author, apool) => { const deletedAttrib = apool.putAttrib(['removed', true]); const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`; - // iteratore over the operators of the changeset - while (iterator.hasNext()) { - const operator = iterator.next(); - + for (const operator of Changeset.deserializeOps(unpacked.ops)) { if (operator.opcode === '-') { // this is a delete operator, extend it with the author operator.attribs = attribs; @@ -268,22 +258,23 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { let curLine = 0; let curChar = 0; - let curLineOpIter = null; - let curLineOpIterLine; + let curLineOps = null; + let curLineOpsNext = null; + let curLineOpsLine; let curLineNextOp = new Changeset.Op('+'); const unpacked = Changeset.unpack(cs); - const csIter = Changeset.opIterator(unpacked.ops); const builder = Changeset.builder(unpacked.newLen); const consumeAttribRuns = (numChars, func /* (len, attribs, endsLine)*/) => { - if ((!curLineOpIter) || (curLineOpIterLine !== curLine)) { - // create curLineOpIter and advance it to curChar - curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); - curLineOpIterLine = curLine; + if (!curLineOps || curLineOpsLine !== curLine) { + curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOpsNext = curLineOps.next(); + curLineOpsLine = curLine; let indexIntoLine = 0; - while (curLineOpIter.hasNext()) { - curLineNextOp = curLineOpIter.next(); + while (!curLineOpsNext.done) { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); break; @@ -293,16 +284,22 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { } while (numChars > 0) { - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; - curLineOpIterLine = curLine; + curLineOpsLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = Changeset.opIterator(aLinesGet(curLine)); + curLineOps = Changeset.deserializeOps(aLinesGet(curLine)); + curLineOpsNext = curLineOps.next(); } if (!curLineNextOp.chars) { - curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : new Changeset.Op(); + if (curLineOpsNext.done) { + curLineNextOp = new Changeset.Op(); + } else { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); + } } const charsToUse = Math.min(numChars, curLineNextOp.chars); @@ -314,7 +311,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { curChar += charsToUse; } - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; } @@ -324,7 +321,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { if (L) { curLine += L; curChar = 0; - } else if (curLineOpIter && curLineOpIterLine === curLine) { + } else if (curLineOps && curLineOpsLine === curLine) { consumeAttribRuns(N, () => {}); } else { curChar += N; @@ -361,10 +358,7 @@ PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) { }; }; - // iterate over all operators of this changeset - while (csIter.hasNext()) { - const csOp = csIter.next(); - + for (const csOp of Changeset.deserializeOps(unpacked.ops)) { if (csOp.opcode === '=') { const textBank = nextText(csOp.chars); diff --git a/src/static/js/AttributeManager.js b/src/static/js/AttributeManager.js index ebef74c0..f508af64 100644 --- a/src/static/js/AttributeManager.js +++ b/src/static/js/AttributeManager.js @@ -150,9 +150,9 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ // get `attributeName` attribute of first char of line const aline = this.rep.alines[lineNum]; if (!aline) return ''; - const opIter = Changeset.opIterator(aline); - if (!opIter.hasNext()) return ''; - return AttributeMap.fromString(opIter.next().attribs, this.rep.apool).get(attributeName) || ''; + const [op] = Changeset.deserializeOps(aline); + if (op == null) return ''; + return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || ''; }, /* @@ -163,9 +163,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ // get attributes of first char of line const aline = this.rep.alines[lineNum]; if (!aline) return []; - const opIter = Changeset.opIterator(aline); - if (!opIter.hasNext()) return []; - const op = opIter.next(); + const [op] = Changeset.deserializeOps(aline); + if (op == null) return []; return [...attributes.attribsFromString(op.attribs, this.rep.apool)]; }, @@ -221,13 +220,8 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ const end = selEnd[1]; let hasAttrib = true; - // Iterate over attribs on this line - - const opIter = Changeset.opIterator(rep.alines[lineNum]); let indexIntoLine = 0; - - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { const opStartInLine = indexIntoLine; const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { @@ -260,15 +254,11 @@ AttributeManager.prototype = _(AttributeManager.prototype).extend({ if (!aline) { return []; } - // iterate through all operations of a line - const opIter = Changeset.opIterator(aline); // we need to sum up how much characters each operations take until the wanted position let currentPointer = 0; - let currentOperation; - while (opIter.hasNext()) { - currentOperation = opIter.next(); + for (const currentOperation of Changeset.deserializeOps(aline)) { currentPointer += currentOperation.chars; if (currentPointer <= column) continue; return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)]; diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index ac19f468..669041ff 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -187,7 +187,7 @@ exports.newLen = (cs) => exports.unpack(cs).newLen; * @yields {Op} * @returns {Generator} */ -const deserializeOps = function* (ops) { +exports.deserializeOps = function* (ops) { // TODO: Migrate to String.prototype.matchAll() once there is enough browser support. const regex = /((?:\*[0-9a-z]+)*)(?:\|([0-9a-z]+))?([-+=])([0-9a-z]+)|(.)/g; let match; @@ -206,13 +206,15 @@ const deserializeOps = function* (ops) { * Iterator over a changeset's operations. * * Note: This class does NOT implement the ECMAScript iterable or iterator protocols. + * + * @deprecated Use `deserializeOps` instead. */ class OpIter { /** * @param {string} ops - String encoding the change operations to iterate over. */ constructor(ops) { - this._gen = deserializeOps(ops); + this._gen = exports.deserializeOps(ops); this._next = this._gen.next(); } @@ -246,10 +248,15 @@ class OpIter { /** * Creates an iterator which decodes string changeset operations. * + * @deprecated Use `deserializeOps` instead. * @param {string} opsStr - String encoding of the change operations to perform. * @returns {OpIter} Operator iterator object. */ -exports.opIterator = (opsStr) => new OpIter(opsStr); +exports.opIterator = (opsStr) => { + padutils.warnWithStack( + 'Changeset.opIterator() is deprecated; use Changeset.deserializeOps() instead'); + return new OpIter(opsStr); +}; /** * Cleans an Op object. @@ -374,9 +381,7 @@ exports.checkRep = (cs) => { let oldPos = 0; let calcNewLen = 0; let numInserted = 0; - const iter = new OpIter(ops); - while (iter.hasNext()) { - const o = iter.next(); + for (const o of exports.deserializeOps(ops)) { switch (o.opcode) { case '=': oldPos += o.chars; @@ -1027,15 +1032,18 @@ class TextLinesMutator { * @returns {string} the integrated changeset */ const applyZip = (in1, in2, func) => { - const iter1 = new OpIter(in1); - const iter2 = new OpIter(in2); + const ops1 = exports.deserializeOps(in1); + const ops2 = exports.deserializeOps(in2); + let next1 = ops1.next(); + let next2 = ops2.next(); const assem = exports.smartOpAssembler(); - const op1 = new Op(); - const op2 = new Op(); - 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); - const opOut = func(op1, op2); + while (!next1.done || !next2.done) { + if (!next1.done && !next1.value.opcode) next1 = ops1.next(); + if (!next2.done && !next2.value.opcode) next2 = ops2.next(); + if (next1.value == null) next1.value = new Op(); + if (next2.value == null) next2.value = new Op(); + if (!next1.value.opcode && !next2.value.opcode) break; + const opOut = func(next1.value, next2.value); if (opOut && opOut.opcode) assem.append(opOut); } assem.endDocument(); @@ -1097,12 +1105,10 @@ 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 = new OpIter(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const strIter = exports.stringIterator(str); const assem = exports.stringAssembler(); - while (csIter.hasNext()) { - const op = csIter.next(); + for (const op of exports.deserializeOps(unpacked.ops)) { switch (op.opcode) { case '+': // op is + and op.lines 0: no newlines must be in op.chars @@ -1142,11 +1148,9 @@ exports.applyToText = (cs, str) => { */ exports.mutateTextLines = (cs, lines) => { const unpacked = exports.unpack(cs); - const csIter = new OpIter(unpacked.ops); const bankIter = exports.stringIterator(unpacked.charBank); const mut = new TextLinesMutator(lines); - while (csIter.hasNext()) { - const op = csIter.next(); + for (const op of exports.deserializeOps(unpacked.ops)) { switch (op.opcode) { case '+': mut.insert(bankIter.take(op.chars), op.lines); @@ -1273,24 +1277,30 @@ exports.applyToAttribution = (cs, astr, pool) => { exports.mutateAttributionLines = (cs, lines, pool) => { const unpacked = exports.unpack(cs); - const csIter = new OpIter(unpacked.ops); + const csOps = exports.deserializeOps(unpacked.ops); + let csOpsNext = csOps.next(); const csBank = unpacked.charBank; let csBankIndex = 0; // treat the attribution lines as text lines, mutating a line at a time const mut = new TextLinesMutator(lines); - /** @type {?OpIter} */ - let lineIter = null; + /** @type {?Generator} */ + let lineOps = null; + let lineOpsNext = null; - const isNextMutOp = () => (lineIter && lineIter.hasNext()) || mut.hasMore(); + const lineOpsHasNext = () => lineOpsNext && !lineOpsNext.done; + const isNextMutOp = () => lineOpsHasNext() || mut.hasMore(); const nextMutOp = () => { - if ((!(lineIter && lineIter.hasNext())) && mut.hasMore()) { + if (!lineOpsHasNext() && mut.hasMore()) { const line = mut.removeLines(1); - lineIter = new OpIter(line); + lineOps = exports.deserializeOps(line); + lineOpsNext = lineOps.next(); } - if (!lineIter || !lineIter.hasNext()) return new Op(); - return lineIter.next(); + if (!lineOpsHasNext()) return new Op(); + const op = lineOpsNext.value; + lineOpsNext = lineOps.next(); + return op; }; let lineAssem = null; @@ -1308,12 +1318,15 @@ exports.mutateAttributionLines = (cs, lines, pool) => { let csOp = new Op(); let attOp = new Op(); - while (csOp.opcode || csIter.hasNext() || attOp.opcode || isNextMutOp()) { - if (!csOp.opcode && csIter.hasNext()) csOp = csIter.next(); - if ((!csOp.opcode) && (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { + while (csOp.opcode || !csOpsNext.done || attOp.opcode || isNextMutOp()) { + if (!csOp.opcode && !csOpsNext.done) { + csOp = csOpsNext.value; + csOpsNext = csOps.next(); + } + if (!csOp.opcode && !attOp.opcode && !lineAssem && !lineOpsHasNext()) { break; // done - } else if (csOp.opcode === '=' && csOp.lines > 0 && (!csOp.attribs) && - (!attOp.opcode) && (!lineAssem) && (!(lineIter && lineIter.hasNext()))) { + } else if (csOp.opcode === '=' && csOp.lines > 0 && !csOp.attribs && !attOp.opcode && + !lineAssem && !lineOpsHasNext()) { // skip multiple lines; this is what makes small changes not order of the document size mut.skipLines(csOp.lines); csOp.opcode = ''; @@ -1350,16 +1363,12 @@ exports.mutateAttributionLines = (cs, lines, pool) => { exports.joinAttributionLines = (theAlines) => { const assem = exports.mergingOpAssembler(); for (const aline of theAlines) { - const iter = new OpIter(aline); - while (iter.hasNext()) { - assem.append(iter.next()); - } + for (const op of exports.deserializeOps(aline)) assem.append(op); } return assem.toString(); }; exports.splitAttributionLines = (attrOps, text) => { - const iter = new OpIter(attrOps); const assem = exports.mergingOpAssembler(); const lines = []; let pos = 0; @@ -1373,8 +1382,7 @@ exports.splitAttributionLines = (attrOps, text) => { pos += op.chars; }; - while (iter.hasNext()) { - const op = iter.next(); + for (const op of exports.deserializeOps(attrOps)) { let numChars = op.chars; let numLines = op.lines; while (numLines > 1) { @@ -1517,11 +1525,9 @@ const toSplices = (cs) => { const splices = []; let oldPos = 0; - const iter = new OpIter(unpacked.ops); const charIter = exports.stringIterator(unpacked.charBank); let inSplice = false; - while (iter.hasNext()) { - const op = iter.next(); + for (const op of exports.deserializeOps(unpacked.ops)) { if (op.opcode === '=') { oldPos += op.chars; inSplice = false; @@ -1764,11 +1770,10 @@ exports.copyAText = (atext1, atext2) => { */ exports.opsFromAText = function* (atext) { // intentionally skips last newline char of atext - const iter = new OpIter(atext.attribs); let lastOp = null; - while (iter.hasNext()) { + for (const op of exports.deserializeOps(atext.attribs)) { if (lastOp != null) yield lastOp; - lastOp = iter.next(); + lastOp = op; } if (lastOp == null) return; // exclude final newline @@ -1986,15 +1991,19 @@ exports.makeAttribsString = (opcode, attribs, pool) => { * Like "substring" but on a single-line attribution string. */ exports.subattribution = (astr, start, optEnd) => { - const iter = new OpIter(astr); + const attOps = exports.deserializeOps(astr); + let attOpsNext = attOps.next(); const assem = exports.smartOpAssembler(); let attOp = new Op(); const csOp = new Op(); const doCsOp = () => { if (!csOp.chars) return; - while (csOp.opcode && (attOp.opcode || iter.hasNext())) { - if (!attOp.opcode) attOp = iter.next(); + while (csOp.opcode && (attOp.opcode || !attOpsNext.done)) { + if (!attOp.opcode) { + attOp = attOpsNext.value; + attOpsNext = attOps.next(); + } if (csOp.opcode && attOp.opcode && csOp.chars >= attOp.chars && attOp.lines > 0 && csOp.lines <= 0) { csOp.lines++; @@ -2013,7 +2022,10 @@ exports.subattribution = (astr, start, optEnd) => { if (attOp.opcode) { assem.append(attOp); } - while (iter.hasNext()) assem.append(iter.next()); + while (!attOpsNext.done) { + assem.append(attOpsNext.value); + attOpsNext = attOps.next(); + } } else { csOp.opcode = '='; csOp.chars = optEnd - start; @@ -2050,22 +2062,23 @@ exports.inverse = (cs, lines, alines, pool) => { let curLine = 0; let curChar = 0; - let curLineOpIter = null; - let curLineOpIterLine; + let curLineOps = null; + let curLineOpsNext = null; + let curLineOpsLine; let curLineNextOp = new Op('+'); const unpacked = exports.unpack(cs); - 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 = new OpIter(alinesGet(curLine)); - curLineOpIterLine = curLine; + if (!curLineOps || curLineOpsLine !== curLine) { + curLineOps = exports.deserializeOps(alinesGet(curLine)); + curLineOpsNext = curLineOps.next(); + curLineOpsLine = curLine; let indexIntoLine = 0; - while (curLineOpIter.hasNext()) { - curLineNextOp = curLineOpIter.next(); + while (!curLineOpsNext.done) { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); if (indexIntoLine + curLineNextOp.chars >= curChar) { curLineNextOp.chars -= (curChar - indexIntoLine); break; @@ -2075,15 +2088,21 @@ exports.inverse = (cs, lines, alines, pool) => { } while (numChars > 0) { - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; - curLineOpIterLine = curLine; + curLineOpsLine = curLine; curLineNextOp.chars = 0; - curLineOpIter = new OpIter(alinesGet(curLine)); + curLineOps = exports.deserializeOps(alinesGet(curLine)); + curLineOpsNext = curLineOps.next(); } if (!curLineNextOp.chars) { - curLineNextOp = curLineOpIter.hasNext() ? curLineOpIter.next() : new Op(); + if (curLineOpsNext.done) { + curLineNextOp = new Op(); + } else { + curLineNextOp = curLineOpsNext.value; + curLineOpsNext = curLineOps.next(); + } } const charsToUse = Math.min(numChars, curLineNextOp.chars); func(charsToUse, curLineNextOp.attribs, charsToUse === curLineNextOp.chars && @@ -2093,7 +2112,7 @@ exports.inverse = (cs, lines, alines, pool) => { curChar += charsToUse; } - if ((!curLineNextOp.chars) && (!curLineOpIter.hasNext())) { + if (!curLineNextOp.chars && curLineOpsNext.done) { curLine++; curChar = 0; } @@ -2103,7 +2122,7 @@ exports.inverse = (cs, lines, alines, pool) => { if (L) { curLine += L; curChar = 0; - } else if (curLineOpIter && curLineOpIterLine === curLine) { + } else if (curLineOps && curLineOpsLine === curLine) { consumeAttribRuns(N, () => {}); } else { curChar += N; @@ -2138,8 +2157,7 @@ exports.inverse = (cs, lines, alines, pool) => { }; }; - while (csIter.hasNext()) { - const csOp = csIter.next(); + for (const csOp of exports.deserializeOps(unpacked.ops)) { if (csOp.opcode === '=') { if (csOp.attribs) { const attribs = AttributeMap.fromString(csOp.attribs, pool); diff --git a/src/static/js/ace2_inner.js b/src/static/js/ace2_inner.js index 484205c0..77912057 100644 --- a/src/static/js/ace2_inner.js +++ b/src/static/js/ace2_inner.js @@ -1576,13 +1576,8 @@ function Ace2Inner(editorInfo, cssManagers) { const end = selEnd[1]; let hasAttrib = true; - // Iterate over attribs on this line - - const opIter = Changeset.opIterator(rep.alines[lineNum]); let indexIntoLine = 0; - - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(rep.alines[lineNum])) { const opStartInLine = indexIntoLine; const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { @@ -1615,7 +1610,6 @@ function Ace2Inner(editorInfo, cssManagers) { const selStartLine = rep.selStart[0]; const selEndLine = rep.selEnd[0]; for (let n = selStartLine; n <= selEndLine; n++) { - const opIter = Changeset.opIterator(rep.alines[n]); let indexIntoLine = 0; let selectionStartInLine = 0; if (documentAttributeManager.lineHasMarker(n)) { @@ -1628,8 +1622,7 @@ function Ace2Inner(editorInfo, cssManagers) { if (n === selEndLine) { selectionEndInLine = rep.selEnd[1]; } - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(rep.alines[n])) { const opStartInLine = indexIntoLine; const opEndInLine = opStartInLine + op.chars; if (!hasIt(op.attribs)) { @@ -1754,12 +1747,10 @@ function Ace2Inner(editorInfo, cssManagers) { }; const eachAttribRun = (attribs, func /* (startInNewText, endInNewText, attribs)*/) => { - const attribsIter = Changeset.opIterator(attribs); let textIndex = 0; const newTextStart = commonStart; const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0); - while (attribsIter.hasNext()) { - const op = attribsIter.next(); + for (const op of Changeset.deserializeOps(attribs)) { const nextIndex = textIndex + op.chars; if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) { func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs); @@ -1873,9 +1864,7 @@ function Ace2Inner(editorInfo, cssManagers) { const attribRuns = (attribs) => { const lengs = []; const atts = []; - const iter = Changeset.opIterator(attribs); - while (iter.hasNext()) { - const op = iter.next(); + for (const op of Changeset.deserializeOps(attribs)) { lengs.push(op.chars); atts.push(op.attribs); } @@ -2619,9 +2608,7 @@ function Ace2Inner(editorInfo, cssManagers) { // TODO: There appears to be a race condition or so. const authorIds = new Set(); if (alineAttrs) { - const opIter = Changeset.opIterator(alineAttrs); - while (opIter.hasNext()) { - const op = opIter.next(); + for (const op of Changeset.deserializeOps(alineAttrs)) { const authorId = AttributeMap.fromString(op.attribs, apool).get('author'); if (authorId) authorIds.add(authorId); } diff --git a/src/static/js/broadcast.js b/src/static/js/broadcast.js index 5b19acf8..4014d582 100644 --- a/src/static/js/broadcast.js +++ b/src/static/js/broadcast.js @@ -162,13 +162,8 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro // some chars are replaced (no attributes change and no length change) // test if there are keep ops at the start of the cs if (lineChanged === undefined) { - lineChanged = 0; - const opIter = Changeset.opIterator(Changeset.unpack(changeset).ops); - - if (opIter.hasNext()) { - const op = opIter.next(); - if (op.opcode === '=') lineChanged += op.lines; - } + const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops); + lineChanged = op != null && op.opcode === '=' ? op.lines : 0; } const goToLineNumber = (lineNumber) => { diff --git a/src/static/js/changesettracker.js b/src/static/js/changesettracker.js index c45a253d..30c70aa7 100644 --- a/src/static/js/changesettracker.js +++ b/src/static/js/changesettracker.js @@ -143,12 +143,9 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => { // Sanitize authorship: Replace all author attributes with this user's author ID in case the // text was copied from another author. const cs = Changeset.unpack(userChangeset); - const iterator = Changeset.opIterator(cs.ops); - let op; const assem = Changeset.mergingOpAssembler(); - while (iterator.hasNext()) { - op = iterator.next(); + for (const op of Changeset.deserializeOps(cs.ops)) { if (op.opcode === '+') { const attribs = AttributeMap.fromString(op.attribs, apool); const oldAuthorId = attribs.get('author'); diff --git a/src/static/js/linestylefilter.js b/src/static/js/linestylefilter.js index 19751999..632e6b3c 100644 --- a/src/static/js/linestylefilter.js +++ b/src/static/js/linestylefilter.js @@ -98,11 +98,13 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool return classes.substring(1); }; - const attributionIter = Changeset.opIterator(aline); + const attrOps = Changeset.deserializeOps(aline); + let attrOpsNext = attrOps.next(); let nextOp, nextOpClasses; const goNextOp = () => { - nextOp = attributionIter.hasNext() ? attributionIter.next() : new Changeset.Op(); + nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value; + if (!attrOpsNext.done) attrOpsNext = attrOps.next(); nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs)); }; goNextOp(); diff --git a/src/tests/frontend/specs/easysync.js b/src/tests/frontend/specs/easysync.js index fc56c62a..5d066dfd 100644 --- a/src/tests/frontend/specs/easysync.js +++ b/src/tests/frontend/specs/easysync.js @@ -31,17 +31,15 @@ const randInt = (maxValue) => Math.floor(Math.random() * maxValue); describe('easysync', function () { it('throughIterator', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - const iter = Changeset.opIterator(x); const assem = Changeset.opAssembler(); - while (iter.hasNext()) assem.append(iter.next()); + for (const op of Changeset.deserializeOps(x)) assem.append(op); expect(assem.toString()).to.equal(x); }); it('throughSmartAssembler', async function () { const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1'; - const iter = Changeset.opIterator(x); const assem = Changeset.smartOpAssembler(); - while (iter.hasNext()) assem.append(iter.next()); + for (const op of Changeset.deserializeOps(x)) assem.append(op); assem.endDocument(); expect(assem.toString()).to.equal(x); }); @@ -730,7 +728,7 @@ describe('easysync', function () { p.putAttrib(['name', 'david']); p.putAttrib(['color', 'green']); - const stringOp = (str) => Changeset.opIterator(str).next(); + const stringOp = (str) => Changeset.deserializeOps(str).next().value; expect(Changeset.opAttributeValue(stringOp('*0*1+1'), 'name', p)).to.equal('david'); expect(Changeset.opAttributeValue(stringOp('*0+1'), 'name', p)).to.equal('david');