Changeset: Turn `opIterator()` into a real class

This commit is contained in:
Richard Hansen 2021-10-13 15:42:03 -04:00
parent 86959f7ebc
commit a4aec006dc
1 changed files with 63 additions and 51 deletions

View File

@ -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();