diff --git a/src/static/js/Changeset.js b/src/static/js/Changeset.js index 47c920e95..98136e012 100644 --- a/src/static/js/Changeset.js +++ b/src/static/js/Changeset.js @@ -319,13 +319,68 @@ class OpAssembler { /** * Efficiently merges consecutive operations that are mergeable, ignores no-ops, and drops final * pure "keeps". It does not re-order operations. - * - * @typedef {object} MergingOpAssembler - * @property {Function} append - - * @property {Function} clear - - * @property {Function} endDocument - - * @property {Function} toString - */ +class MergingOpAssembler { + constructor() { + this._assem = new OpAssembler(); + this._bufOp = new Op(); + // 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. + this._bufOpAdditionalCharsAfterNewline = 0; + } + + clear() { + this._assem.clear(); + clearOp(this._bufOp); + } + + _flush(isEndDocument) { + if (!this._bufOp.opcode) return; + if (isEndDocument && this._bufOp.opcode === '=' && !this._bufOp.attribs) { + // final merged keep, leave it implicit + } else { + this._assem.append(this._bufOp); + if (this._bufOpAdditionalCharsAfterNewline) { + this._bufOp.chars = this._bufOpAdditionalCharsAfterNewline; + this._bufOp.lines = 0; + this._assem.append(this._bufOp); + this._bufOpAdditionalCharsAfterNewline = 0; + } + } + this._bufOp.opcode = ''; + } + + append(op) { + if (op.chars <= 0) return; + if (this._bufOp.opcode === op.opcode && this._bufOp.attribs === op.attribs) { + if (op.lines > 0) { + // this._bufOp and additional chars are all mergeable into a multi-line op + this._bufOp.chars += this._bufOpAdditionalCharsAfterNewline + op.chars; + this._bufOp.lines += op.lines; + this._bufOpAdditionalCharsAfterNewline = 0; + } else if (this._bufOp.lines === 0) { + // both this._bufOp and op are in-line + this._bufOp.chars += op.chars; + } else { + // append in-line text to multi-line this._bufOp + this._bufOpAdditionalCharsAfterNewline += op.chars; + } + } else { + this._flush(); + copyOp(op, this._bufOp); + } + } + + endDocument() { + this._flush(true); + } + + toString() { + this._flush(); + return this._assem.toString(); + } +} /** * Generates operations from the given text and attributes. @@ -438,9 +493,9 @@ exports.checkRep = (cs) => { * @returns {SmartOpAssembler} */ exports.smartOpAssembler = () => { - const minusAssem = exports.mergingOpAssembler(); - const plusAssem = exports.mergingOpAssembler(); - const keepAssem = exports.mergingOpAssembler(); + const minusAssem = new MergingOpAssembler(); + const plusAssem = new MergingOpAssembler(); + const keepAssem = new MergingOpAssembler(); const assem = exports.stringAssembler(); let lastOpcode = ''; let lengthChange = 0; @@ -531,73 +586,7 @@ exports.smartOpAssembler = () => { /** * @returns {MergingOpAssembler} */ -exports.mergingOpAssembler = () => { - const assem = exports.opAssembler(); - const bufOp = new Op(); - - // 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. - let bufOpAdditionalCharsAfterNewline = 0; - - const flush = (isEndDocument) => { - if (!bufOp.opcode) return; - 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 = ''; - }; - - const append = (op) => { - if (op.chars <= 0) return; - 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(); - copyOp(op, bufOp); - } - }; - - const endDocument = () => { - flush(true); - }; - - const toString = () => { - flush(); - return assem.toString(); - }; - - const clear = () => { - assem.clear(); - clearOp(bufOp); - }; - return { - append, - toString, - clear, - endDocument, - }; -}; +exports.mergingOpAssembler = () => new MergingOpAssembler(); /** * @returns {OpAssembler} @@ -1301,9 +1290,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { let lineAssem = null; const outputMutOp = (op) => { - if (!lineAssem) { - lineAssem = exports.mergingOpAssembler(); - } + if (!lineAssem) lineAssem = new MergingOpAssembler(); lineAssem.append(op); if (op.lines <= 0) return; assert(op.lines === 1, `Can't have op.lines of ${op.lines} in attribution lines`); @@ -1357,7 +1344,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => { * @returns {string} joined Attribution lines */ exports.joinAttributionLines = (theAlines) => { - const assem = exports.mergingOpAssembler(); + const assem = new MergingOpAssembler(); for (const aline of theAlines) { for (const op of exports.deserializeOps(aline)) assem.append(op); } @@ -1365,7 +1352,7 @@ exports.joinAttributionLines = (theAlines) => { }; exports.splitAttributionLines = (attrOps, text) => { - const assem = exports.mergingOpAssembler(); + const assem = new MergingOpAssembler(); const lines = []; let pos = 0;