Feat/changeset ts (#6594)

* Migrated changeset

* Added more tests.

* Fixed test scopes
This commit is contained in:
SamTV12345 2024-08-18 12:14:24 +02:00 committed by GitHub
parent 3dae23a1e5
commit 28e04bdf71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 2540 additions and 1310 deletions

View file

@ -1,11 +1,10 @@
// @ts-nocheck
'use strict';
import AttributeMap from './AttributeMap';
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const attributes = require('./attributes');
const underscore = require("underscore")
import {compose, deserializeOps, isIdentity} from './Changeset';
import {Builder} from "./Builder";
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils';
import attributes from './attributes';
import underscore from "underscore";
const lineMarkerAttribute = 'lmkr';
@ -52,7 +51,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
if (!this.applyChangesetCallback) return changeset;
const cs = changeset.toString();
if (!Changeset.isIdentity(cs)) {
if (!isIdentity(cs)) {
this.applyChangesetCallback(cs);
}
@ -86,7 +85,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// as the range might not be continuous
// due to the presence of line markers on the rows
if (allChangesets) {
allChangesets = Changeset.compose(
allChangesets = compose(
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
} else {
allChangesets = rowChangeset;
@ -126,9 +125,9 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
* @param attribs an array of attributes
*/
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
ChangesetUtils.buildKeepRange(
const builder = new Builder(this.rep.lines.totalWidth());
buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
buildKeepRange(
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
return builder;
},
@ -151,7 +150,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// get `attributeName` attribute of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return '';
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op == null) return '';
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
},
@ -164,7 +163,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// get attributes of first char of line
const aline = this.rep.alines[lineNum];
if (!aline) return [];
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op == null) return [];
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
},
@ -222,7 +221,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
let hasAttrib = true;
let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
for (const op of deserializeOps(rep.alines[lineNum])) {
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
@ -259,7 +258,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
// we need to sum up how much characters each operations take until the wanted position
let currentPointer = 0;
for (const currentOperation of Changeset.deserializeOps(aline)) {
for (const currentOperation of deserializeOps(aline)) {
currentPointer += currentOperation.chars;
if (currentPointer <= column) continue;
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
@ -286,13 +285,13 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
*/
setAttributeOnLine(lineNum, attributeName, attributeValue) {
let loc = [0, 0];
const builder = Changeset.builder(this.rep.lines.totalWidth());
const builder = new Builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum);
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
if (hasMarker) {
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
[attributeName, attributeValue],
], this.rep.apool);
} else {
@ -315,7 +314,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
* @param attributeValue if given only attributes with equal value will be removed
*/
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
const builder = Changeset.builder(this.rep.lines.totalWidth());
const builder = new Builder(this.rep.lines.totalWidth());
const hasMarker = this.lineHasMarker(lineNum);
let found = false;
@ -334,16 +333,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
return;
}
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
// if we have marker and any of attributes don't need to have marker. we need delete it
if (hasMarker && !countAttribsWithMarker) {
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
} else {
ChangesetUtils.buildKeepRange(
buildKeepRange(
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
}

108
src/static/js/Builder.ts Normal file
View file

@ -0,0 +1,108 @@
/**
* Incrementally builds a Changeset.
*
* @typedef {object} Builder
* @property {Function} insert -
* @property {Function} keep -
* @property {Function} keepText -
* @property {Function} remove -
* @property {Function} toString -
*/
import {SmartOpAssembler} from "./SmartOpAssembler";
import Op from "./Op";
import {StringAssembler} from "./StringAssembler";
import AttributeMap from "./AttributeMap";
import {Attribute} from "./types/Attribute";
import AttributePool from "./AttributePool";
import {opsFromText, pack} from "./Changeset";
/**
* @param {number} oldLen - Old length
* @returns {Builder}
*/
export class Builder {
private readonly oldLen: number;
private assem: SmartOpAssembler;
private readonly o: Op;
private charBank: StringAssembler;
constructor(oldLen: number) {
this.oldLen = oldLen
this.assem = new SmartOpAssembler()
this.o = new Op()
this.charBank = new StringAssembler()
}
/**
* @param {number} N - Number of characters to keep.
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
* character must be a newline.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
keep = (N: number, L?: number, attribs?: string|Attribute[], pool?: AttributePool): Builder => {
this.o.opcode = '=';
this.o.attribs = typeof attribs === 'string'
? attribs : new AttributeMap(pool).update(attribs || []).toString();
this.o.chars = N;
this.o.lines = (L || 0);
this.assem.append(this.o);
return this;
}
/**
* @param {string} text - Text to keep.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
keepText= (text: string, attribs?: string|Attribute[], pool?: AttributePool): Builder=> {
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
return this;
}
/**
* @param {string} text - Text to insert.
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
* (no pool needed in latter case).
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
* attribute key, value pairs.
* @returns {Builder} this
*/
insert= (text: string, attribs: string | Attribute[] | undefined, pool?: AttributePool | null | undefined): Builder => {
for (const op of opsFromText('+', text, attribs, pool)) this.assem.append(op);
this.charBank.append(text);
return this;
}
/**
* @param {number} N - Number of characters to remove.
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
* character must be a newline.
* @returns {Builder} this
*/
remove= (N: number, L?: number): Builder => {
this.o.opcode = '-';
this.o.attribs = '';
this.o.chars = N;
this.o.lines = (L || 0);
this.assem.append(this.o);
return this;
}
toString= () => {
this.assem.endDocument();
const newLen = this.oldLen + this.assem.getLengthChange();
return pack(this.oldLen, newLen, this.assem.toString(), this.charBank.toString());
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
// @ts-nocheck
'use strict';
/**
@ -6,6 +5,12 @@
* based on a SkipList
*/
import {RepModel} from "./types/RepModel";
import {ChangeSetBuilder} from "./types/ChangeSetBuilder";
import {Attribute} from "./types/Attribute";
import AttributePool from "./AttributePool";
import {Builder} from "./Builder";
/**
* Copyright 2009 Google Inc.
*
@ -21,7 +26,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
exports.buildRemoveRange = (rep, builder, start, end) => {
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -33,7 +38,7 @@ exports.buildRemoveRange = (rep, builder, start, end) => {
}
};
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
export const buildKeepRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number], end:[number, number], attribs?: Attribute[], pool?: AttributePool) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
@ -45,9 +50,25 @@ exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
}
};
exports.buildKeepToStartOfRange = (rep, builder, start) => {
export const buildKeepToStartOfRange = (rep: RepModel, builder: Builder, start: [number, number]) => {
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
builder.keep(startLineOffset, start[0]);
builder.keep(start[1]);
};
/**
* Parses a number from string base 36.
*
* @param {string} str - string of the number in base 36
* @returns {number} number
*/
export const parseNum = (str: string): number => parseInt(str, 36);
/**
* Writes a number in base 36 and puts it in a string.
*
* @param {number} num - number
* @returns {string} string
*/
export const numToString = (num: number): string => num.toString(36).toLowerCase();

View file

@ -0,0 +1,73 @@
import {OpAssembler} from "./OpAssembler";
import Op from "./Op";
import {clearOp, copyOp} from "./Changeset";
export class MergingOpAssembler {
private assem: OpAssembler;
private readonly bufOp: Op;
private bufOpAdditionalCharsAfterNewline: number;
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;
}
/**
* @param {boolean} [isEndDocument]
*/
flush = (isEndDocument?: boolean) => {
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: Op) => {
if (op.chars <= 0) return;
if (this.bufOp.opcode === op.opcode && this.bufOp.attribs === op.attribs) {
if (op.lines > 0) {
// 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 bufOp and op are in-line
this.bufOp.chars += op.chars;
} else {
// append in-line text to multi-line bufOp
this.bufOpAdditionalCharsAfterNewline += op.chars;
}
} else {
this.flush();
copyOp(op, this.bufOp);
}
}
endDocument = () => {
this.flush(true);
};
toString = () => {
this.flush();
return this.assem.toString();
};
clear = () => {
this.assem.clear();
clearOp(this.bufOp);
};
}

78
src/static/js/Op.ts Normal file
View file

@ -0,0 +1,78 @@
import {numToString} from "./ChangesetUtils";
export type OpCode = ''|'='|'+'|'-';
/**
* An operation to apply to a shared document.
*/
export default class Op {
opcode: ''|'='|'+'|'-'
chars: number
lines: number
attribs: string
/**
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
*/
constructor(opcode:''|'='|'+'|'-' = '') {
/**
* The operation's operator:
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
* document.
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
* the document. The inserted characters come from the changeset's character bank.
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
* operation.
*
* @type {(''|'='|'+'|'-')}
* @public
*/
this.opcode = opcode;
/**
* The number of characters to keep, insert, or delete.
*
* @type {number}
* @public
*/
this.chars = 0;
/**
* The number of characters among the `chars` characters that are newlines. If non-zero, the
* last character must be a newline.
*
* @type {number}
* @public
*/
this.lines = 0;
/**
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
* identifiers come from the document's attribute pool.
*
* For keep ('=') operations, the attributes are merged with the base text's existing
* attributes:
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
* has the same key.
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
* existing base text attribute that has the same key, if one exists.
*
* This is the empty string for remove ('-') operations.
*
* @type {string}
* @public
*/
this.attribs = '';
}
toString() {
if (!this.opcode) throw new TypeError('null op');
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
const l = this.lines ? `|${numToString(this.lines)}` : '';
return this.attribs + l + this.opcode + numToString(this.chars);
}
}

View file

@ -0,0 +1,21 @@
import Op from "./Op";
import {assert} from './Changeset'
/**
* @returns {OpAssembler}
*/
export class OpAssembler {
private serialized: string;
constructor() {
this.serialized = ''
}
append = (op: Op) => {
assert(op instanceof Op, 'argument must be an instance of Op');
this.serialized += op.toString();
}
toString = () => this.serialized
clear = () => {
this.serialized = '';
}
}

47
src/static/js/OpIter.ts Normal file
View file

@ -0,0 +1,47 @@
import Op from "./Op";
import {clearOp, copyOp, deserializeOps} from "./Changeset";
/**
* Iterator over a changeset's operations.
*
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
*
* @deprecated Use `deserializeOps` instead.
*/
export class OpIter {
private gen
private _next: IteratorResult<Op, void>
/**
* @param {string} ops - String encoding the change operations to iterate over.
*/
constructor(ops: string) {
this.gen = deserializeOps(ops);
this._next = this.gen.next();
}
/**
* @returns {boolean} Whether there are any remaining operations.
*/
hasNext(): boolean {
return !this._next.done;
}
/**
* 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: Op = new Op()): Op {
if (this.hasNext()) {
copyOp(this._next.value!, opOut);
this._next = this.gen.next();
} else {
clearOp(opOut);
}
return opOut;
}
}

View file

@ -0,0 +1,115 @@
import {MergingOpAssembler} from "./MergingOpAssembler";
import {StringAssembler} from "./StringAssembler";
import padutils from "./pad_utils";
import Op from "./Op";
import { Attribute } from "./types/Attribute";
import AttributePool from "./AttributePool";
import {opsFromText} from "./Changeset";
/**
* Creates an object that allows you to append operations (type Op) and also compresses them if
* possible. Like MergingOpAssembler, but able to produce conforming exportss 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 MergingOpAssembler doesn't do)
*
* @typedef {object} SmartOpAssembler
* @property {Function} append -
* @property {Function} appendOpWithText -
* @property {Function} clear -
* @property {Function} endDocument -
* @property {Function} getLengthChange -
* @property {Function} toString -
*/
export class SmartOpAssembler {
private minusAssem: MergingOpAssembler;
private plusAssem: MergingOpAssembler;
private keepAssem: MergingOpAssembler;
private lastOpcode: string;
private lengthChange: number;
private assem: StringAssembler;
constructor() {
this.minusAssem = new MergingOpAssembler()
this.plusAssem = new MergingOpAssembler()
this.keepAssem = new MergingOpAssembler()
this.assem = new StringAssembler()
this.lastOpcode = ''
this.lengthChange = 0
}
flushKeeps = () => {
this.assem.append(this.keepAssem.toString());
this.keepAssem.clear();
};
flushPlusMinus = () => {
this.assem.append(this.minusAssem.toString());
this.minusAssem.clear();
this.assem.append(this.plusAssem.toString());
this.plusAssem.clear();
};
append = (op: Op) => {
if (!op.opcode) return;
if (!op.chars) return;
if (op.opcode === '-') {
if (this.lastOpcode === '=') {
this.flushKeeps();
}
this.minusAssem.append(op);
this.lengthChange -= op.chars;
} else if (op.opcode === '+') {
if (this.lastOpcode === '=') {
this.flushKeeps();
}
this.plusAssem.append(op);
this.lengthChange += op.chars;
} else if (op.opcode === '=') {
if (this.lastOpcode !== '=') {
this.flushPlusMinus();
}
this.keepAssem.append(op);
}
this.lastOpcode = op.opcode;
};
/**
* Generates operations from the given text and attributes.
*
* @deprecated Use `opsFromText` instead.
* @param {('-'|'+'|'=')} opcode - The operator to use.
* @param {string} text - The text to remove/add/keep.
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
* @param {?AttributePool.ts} pool - Attribute pool. Only required if `attribs` is an iterable of
* attribute key, value pairs.
*/
appendOpWithText = (opcode: '-'|'+'|'=', text: string, attribs: Attribute[]|string, pool?: AttributePool) => {
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
'use opsFromText() instead.');
for (const op of opsFromText(opcode, text, attribs, pool)) this.append(op);
};
toString = () => {
this.flushPlusMinus();
this.flushKeeps();
return this.assem.toString();
};
clear = () => {
this.minusAssem.clear();
this.plusAssem.clear();
this.keepAssem.clear();
this.assem.clear();
this.lengthChange = 0;
};
endDocument = () => {
this.keepAssem.endDocument();
};
getLengthChange = () => this.lengthChange;
}

View file

@ -0,0 +1,18 @@
/**
* @returns {StringAssembler}
*/
export class StringAssembler {
private str = ''
clear = ()=> {
this.str = '';
}
/**
* @param {string} x -
*/
append(x: string) {
this.str += String(x);
}
toString() {
return this.str
}
}

View file

@ -0,0 +1,54 @@
import {assert} from "./Changeset";
/**
* A custom made String Iterator
*
* @typedef {object} StringIterator
* @property {Function} newlines -
* @property {Function} peek -
* @property {Function} remaining -
* @property {Function} skip -
* @property {Function} take -
*/
/**
* @param {string} str - String to iterate over
* @returns {StringIterator}
*/
export class StringIterator {
private curIndex: number;
private newLines: number;
private str: String
constructor(str: string) {
this.curIndex = 0;
this.str = str
this.newLines = str.split('\n').length - 1;
}
remaining = () => this.str.length - this.curIndex;
getnewLines = () => this.newLines;
assertRemaining = (n: number) => {
assert(n <= this.remaining(), `!(${n} <= ${this.remaining()})`);
}
take = (n: number) => {
this.assertRemaining(n);
const s = this.str.substring(this.curIndex, this.curIndex+n);
this.newLines -= s.split('\n').length - 1;
this.curIndex += n;
return s;
}
peek = (n: number) => {
this.assertRemaining(n);
return this.str.substring(this.curIndex, this.curIndex+n);
}
skip = (n: number) => {
this.assertRemaining(n);
this.curIndex += n;
}
}

View file

@ -0,0 +1,348 @@
import {splitTextLines} from "./Changeset";
/**
* Class to iterate and modify texts which have several lines. It is used for applying Changesets on
* arrays of lines.
*
* Mutation operations have the same constraints as exports 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 the `insert` method should be a single line
* with no newlines.
*/
class TextLinesMutator {
private _lines: string[];
private _curSplice: [number, number?];
private _inSplice: boolean;
private _curLine: number;
private _curCol: number;
/**
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
*/
constructor(lines: string[]) {
this._lines = lines;
/**
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
* insert, delete, or change lines:
* - this._curSplice[0] is an index into the this._lines array.
* - this._curSplice[1] is the number of lines that will be removed from the this._lines array
* starting at the index.
* - The other elements represent mutated (changed by ops) lines or new lines (added by ops)
* to insert at the index.
*
* @type {[number, number?, ...string[]?]}
*/
this._curSplice = [0, 0];
this._inSplice = false;
// position in lines after curSplice is applied:
this._curLine = 0;
this._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
}
/**
* Get a line from `lines` at given index.
*
* @param {number} idx - an index
* @returns {string}
*/
_linesGet(idx: number) {
if ('get' in this._lines) {
// @ts-ignore
return this._lines.get(idx) as string;
} else {
return this._lines[idx];
}
}
/**
* Return a slice from `lines`.
*
* @param {number} start - the start index
* @param {number} end - the end index
* @returns {string[]}
*/
_linesSlice(start: number | undefined, end: number | undefined) {
// can be unimplemented if removeLines's return value not needed
if (this._lines.slice) {
return this._lines.slice(start, end);
} else {
return [];
}
}
/**
* Return the length of `lines`.
*
* @returns {number}
*/
_linesLength() {
if (typeof this._lines.length === 'number') {
return this._lines.length;
} else {
// @ts-ignore
return this._lines.length();
}
}
/**
* Starts a new splice.
*/
_enterSplice() {
this._curSplice[0] = this._curLine;
this._curSplice[1] = 0;
// TODO(doc) when is this the case?
// check all enterSplice calls and changes to curCol
if (this._curCol > 0) this._putCurLineInSplice();
this._inSplice = true;
}
/**
* Changes the lines array according to the values in curSplice and resets curSplice. Called via
* close or TODO(doc).
*/
_leaveSplice() {
this._lines.splice(...this._curSplice);
this._curSplice.length = 2;
this._curSplice[0] = this._curSplice[1] = 0;
this._inSplice = false;
}
/**
* Indicates if curLine is already in the splice. This is necessary because the last element in
* curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting).
*
* @returns {boolean} true if curLine is in splice
*/
_isCurLineInSplice() {
// The value of `this._curSplice[1]` does not matter when determining the return value because
// `this._curLine` refers to the line number *after* the splice is applied (so after those lines
// are deleted).
return this._curLine - this._curSplice[0] < this._curSplice.length - 2;
}
/**
* Incorporates current line into the splice and marks its old position to be deleted.
*
* @returns {number} the index of the added line in curSplice
*/
_putCurLineInSplice() {
if (!this._isCurLineInSplice()) {
// @ts-ignore
this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1]));
// @ts-ignore
this._curSplice[1]++;
}
// TODO should be the same as this._curSplice.length - 1
return 2 + this._curLine - this._curSplice[0];
}
/**
* It will skip some newlines by putting them into the splice.
*
* @param {number} L -
* @param {boolean} includeInSplice - Indicates that attributes are present.
*/
skipLines(L: number, includeInSplice?: any) {
if (!L) return;
if (includeInSplice) {
if (!this._inSplice) this._enterSplice();
// TODO(doc) should this count the number of characters that are skipped to check?
for (let i = 0; i < L; i++) {
this._curCol = 0;
this._putCurLineInSplice();
this._curLine++;
}
} else {
if (this._inSplice) {
if (L > 1) {
// TODO(doc) figure out why single lines are incorporated into splice instead of ignored
this._leaveSplice();
} else {
this._putCurLineInSplice();
}
}
this._curLine += L;
this._curCol = 0;
}
// tests case foo in remove(), which isn't otherwise covered in current impl
}
/**
* Skip some characters. Can contain newlines.
*
* @param {number} N - number of characters to skip
* @param {number} L - number of newlines to skip
* @param {boolean} includeInSplice - indicates if attributes are present
*/
skip(N: number, L: number, includeInSplice?: any) {
if (!N) return;
if (L) {
this.skipLines(L, includeInSplice);
} else {
if (includeInSplice && !this._inSplice) this._enterSplice();
if (this._inSplice) {
// although the line is put into splice curLine is not increased, because
// only some chars are skipped, not the whole line
this._putCurLineInSplice();
}
this._curCol += N;
}
}
/**
* Remove whole lines from lines array.
*
* @param {number} L - number of lines to remove
* @returns {string}
*/
removeLines(L: number) {
if (!L) return '';
if (!this._inSplice) this._enterSplice();
/**
* Gets a string of joined lines after the end of the splice.
*
* @param {number} k - number of lines
* @returns {string} joined lines
*/
const nextKLinesText = (k: number) => {
// @ts-ignore
const m = this._curSplice[0] + this._curSplice[1];
return this._linesSlice(m, m + k).join('');
};
let removed = '';
if (this._isCurLineInSplice()) {
if (this._curCol === 0) {
// @ts-ignore
removed = this._curSplice[this._curSplice.length - 1];
this._curSplice.length--;
removed += nextKLinesText(L - 1);
// @ts-ignore
this._curSplice[1] += L - 1;
} else {
removed = nextKLinesText(L - 1);
// @ts-ignore
this._curSplice[1] += L - 1;
const sline = this._curSplice.length - 1;
// @ts-ignore
removed = this._curSplice[sline].substring(this._curCol) + removed;
// @ts-ignore
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
// @ts-ignore
this._linesGet(this._curSplice[0] + this._curSplice[1]);
// @ts-ignore
this._curSplice[1] += 1;
}
} else {
removed = nextKLinesText(L);
this._curSplice[1]! += L;
}
return removed;
}
/**
* Remove text from lines array.
*
* @param {number} N - characters to delete
* @param {number} L - lines to delete
* @returns {string}
*/
remove(N: number, L: any) {
if (!N) return '';
if (L) return this.removeLines(L);
if (!this._inSplice) this._enterSplice();
// although the line is put into splice, curLine is not increased, because
// only some chars are removed not the whole line
const sline = this._putCurLineInSplice();
// @ts-ignore
const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N);
// @ts-ignore
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
// @ts-ignore
this._curSplice[sline].substring(this._curCol + N);
return removed;
}
/**
* Inserts text into lines array.
*
* @param {string} text - the text to insert
* @param {number} L - number of newlines in text
*/
insert(text: string | any[], L: any) {
if (!text) return;
if (!this._inSplice) this._enterSplice();
if (L) {
// @ts-ignore
const newLines = splitTextLines(text);
if (this._isCurLineInSplice()) {
const sline = this._curSplice.length - 1;
/** @type {string} */
const theLine = this._curSplice[sline];
const lineCol = this._curCol;
// Insert the chars up to `curCol` and the first new line.
// @ts-ignore
this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
this._curLine++;
newLines!.splice(0, 1);
// insert the remaining new lines
// @ts-ignore
this._curSplice.push(...newLines);
this._curLine += newLines!.length;
// insert the remaining chars from the "old" line (e.g. the line we were in
// when we started to insert new lines)
// @ts-ignore
this._curSplice.push(theLine.substring(lineCol));
this._curCol = 0; // TODO(doc) why is this not set to the length of last line?
} else {
this._curSplice.push(...newLines);
this._curLine += newLines!.length;
}
} else {
// There are no additional lines. Although the line is put into splice, curLine is not
// increased because there may be more chars in the line (newline is not reached).
const sline = this._putCurLineInSplice();
if (!this._curSplice[sline]) {
const err = new Error(
'curSplice[sline] not populated, actual curSplice contents is ' +
`${JSON.stringify(this._curSplice)}. Possibly related to ` +
'https://github.com/ether/etherpad-lite/issues/2802');
console.error(err.stack || err.toString());
}
// @ts-ignore
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text +
// @ts-ignore
this._curSplice[sline].substring(this._curCol);
this._curCol += text.length;
}
}
/**
* Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`.
*
* @returns {boolean} indicates if there are lines left
*/
hasMore() {
let docLines = this._linesLength();
if (this._inSplice) {
// @ts-ignore
docLines += this._curSplice.length - 2 - this._curSplice[1];
}
return this._curLine < docLines;
}
/**
* Closes the splice
*/
close() {
if (this._inSplice) this._leaveSplice();
}
}
export default TextLinesMutator

View file

@ -1,5 +1,5 @@
// @ts-nocheck
'use strict';
import {Builder} from "./Builder";
/**
* Copyright 2009 Google Inc.
@ -24,6 +24,8 @@ const browser = require('./vendors/browser');
import padutils from './pad_utils'
const Ace2Common = require('./ace2_common');
const $ = require('./rjquery').$;
import {characterRangeFollow, checkRep, cloneAText, compose, deserializeOps, filterAttribNumbers, inverse, isIdentity, makeAText, makeAttribution, mapAttribNumbers, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, opsFromAText, pack, splitAttributionLines} from './Changeset'
const isNodeText = Ace2Common.isNodeText;
const getAssoc = Ace2Common.getAssoc;
@ -33,14 +35,15 @@ const hooks = require('./pluginfw/hooks');
import SkipList from "./skiplist";
import Scroll from './scroll'
import AttribPool from './AttributePool'
import {SmartOpAssembler} from "./SmartOpAssembler";
import Op from "./Op";
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils'
function Ace2Inner(editorInfo, cssManagers) {
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
const colorutils = require('./colorutils').colorutils;
const makeContentCollector = require('./contentcollector').makeContentCollector;
const domline = require('./domline').domline;
const Changeset = require('./Changeset');
const ChangesetUtils = require('./ChangesetUtils');
const linestylefilter = require('./linestylefilter').linestylefilter;
const undoModule = require('./undomodule').undoModule;
const AttributeManager = require('./AttributeManager');
@ -174,9 +177,9 @@ function Ace2Inner(editorInfo, cssManagers) {
// CCCCCCCCCCCCCCCCCCCC\n
// CCCC\n
// end[0]: <CCC end[1] CCC>-------\n
const builder = Changeset.builder(rep.lines.totalWidth());
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
const builder = new Builder(rep.lines.totalWidth());
buildKeepToStartOfRange(rep, builder, start);
buildRemoveRange(rep, builder, start, end);
builder.insert(newText, [
['author', thisAuthor],
], rep.apool);
@ -495,10 +498,10 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const importAText = (atext, apoolJsonObj, undoable) => {
atext = Changeset.cloneAText(atext);
atext = cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
atext.attribs = moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
}
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
setDocAText(atext);
@ -527,18 +530,18 @@ function Ace2Inner(editorInfo, cssManagers) {
const numLines = rep.lines.length();
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
const assem = Changeset.smartOpAssembler();
const o = new Changeset.Op('-');
const assem = new SmartOpAssembler();
const o = new Op('-');
o.chars = upToLastLine;
o.lines = numLines - 1;
assem.append(o);
o.chars = lastLineLength;
o.lines = 0;
assem.append(o);
for (const op of Changeset.opsFromAText(atext)) assem.append(op);
for (const op of opsFromAText(atext)) assem.append(op);
const newLen = oldLen + assem.getLengthChange();
const changeset = Changeset.checkRep(
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
const changeset = checkRep(
pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
performDocumentApplyChangeset(changeset);
performSelectionChange(
@ -552,7 +555,7 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const setDocText = (text) => {
setDocAText(Changeset.makeAText(text));
setDocAText(makeAText(text));
};
const getDocText = () => {
@ -1271,7 +1274,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
theIndent += THE_TAB;
}
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
const cs = new Builder(rep.lines.totalWidth()).keep(
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
theIndent, [
['author', thisAuthor],
@ -1423,7 +1426,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
const result =
Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
}
@ -1435,7 +1438,7 @@ function Ace2Inner(editorInfo, cssManagers) {
length: () => rep.lines.length(),
};
Changeset.mutateTextLines(changes, linesMutatee);
mutateTextLines(changes, linesMutatee);
if (requiredSelectionSetting) {
performSelectionChange(
@ -1446,10 +1449,10 @@ function Ace2Inner(editorInfo, cssManagers) {
};
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
Changeset.checkRep(changes);
checkRep(changes);
if (Changeset.oldLen(changes) !== rep.alltext.length) {
const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`;
if (oldLen(changes) !== rep.alltext.length) {
const errMsg = `${oldLen(changes)}/${rep.alltext.length}`;
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
}
@ -1458,10 +1461,10 @@ function Ace2Inner(editorInfo, cssManagers) {
if (!editEvent.changeset) {
editEvent.changeset = changes;
} else {
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
editEvent.changeset = compose(editEvent.changeset, changes, rep.apool);
}
} else {
const inverseChangeset = Changeset.inverse(changes, {
const inverseChangeset = inverse(changes, {
get: (i) => `${rep.lines.atIndex(i).text}\n`,
length: () => rep.lines.length(),
}, rep.alines, rep.apool);
@ -1469,11 +1472,11 @@ function Ace2Inner(editorInfo, cssManagers) {
if (!editEvent.backset) {
editEvent.backset = inverseChangeset;
} else {
editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
editEvent.backset = compose(inverseChangeset, editEvent.backset, rep.apool);
}
}
Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
mutateAttributionLines(changes, rep.alines, rep.apool);
if (changesetTracker.isTracking()) {
changesetTracker.composeUserChangeset(changes);
@ -1582,7 +1585,7 @@ function Ace2Inner(editorInfo, cssManagers) {
let hasAttrib = true;
let indexIntoLine = 0;
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
for (const op of deserializeOps(rep.alines[lineNum])) {
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
@ -1627,7 +1630,7 @@ function Ace2Inner(editorInfo, cssManagers) {
if (n === selEndLine) {
selectionEndInLine = rep.selEnd[1];
}
for (const op of Changeset.deserializeOps(rep.alines[n])) {
for (const op of deserializeOps(rep.alines[n])) {
const opStartInLine = indexIntoLine;
const opEndInLine = opStartInLine + op.chars;
if (!hasIt(op.attribs)) {
@ -1745,7 +1748,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
const startBuilder = () => {
const builder = Changeset.builder(oldLen);
const builder = new Builder(oldLen);
builder.keep(spliceStartLineStart, spliceStartLine);
builder.keep(spliceStart - spliceStartLineStart);
return builder;
@ -1755,7 +1758,7 @@ function Ace2Inner(editorInfo, cssManagers) {
let textIndex = 0;
const newTextStart = commonStart;
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
for (const op of Changeset.deserializeOps(attribs)) {
for (const op of deserializeOps(attribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
@ -1773,7 +1776,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// changeset the applies the styles found in the DOM.
// This allows us to incorporate, e.g., Safari's native "unbold".
const incorpedAttribClearer = cachedStrFunc(
(oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
(oldAtts) => mapAttribNumbers(oldAtts, (n) => {
const k = rep.apool.getAttribKey(n);
if (isStyleAttribute(k)) {
return rep.apool.putAttrib([k, '']);
@ -1799,7 +1802,7 @@ function Ace2Inner(editorInfo, cssManagers) {
});
const styler = builder2.toString();
theChangeset = Changeset.compose(clearer, styler, rep.apool);
theChangeset = compose(clearer, styler, rep.apool);
} else {
const builder = startBuilder();
@ -1869,7 +1872,7 @@ function Ace2Inner(editorInfo, cssManagers) {
const attribRuns = (attribs) => {
const lengs = [];
const atts = [];
for (const op of Changeset.deserializeOps(attribs)) {
for (const op of deserializeOps(attribs)) {
lengs.push(op.chars);
atts.push(op.attribs);
}
@ -1898,8 +1901,8 @@ function Ace2Inner(editorInfo, cssManagers) {
const newLen = newText.length;
const minLen = Math.min(oldLen, newLen);
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
const oldARuns = attribRuns(filterAttribNumbers(oldAttribs, incorpedAttribFilter));
const newARuns = attribRuns(filterAttribNumbers(newAttribs, incorpedAttribFilter));
let commonStart = 0;
const oldStartIter = attribIterator(oldARuns, false);
@ -2297,7 +2300,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// 3-renumber every list item of the same level from the beginning, level 1
// IMPORTANT: never skip a level because there imbrication may be arbitrary
const builder = Changeset.builder(rep.lines.totalWidth());
const builder = new Builder(rep.lines.totalWidth());
let loc = [0, 0];
const applyNumberList = (line, level) => {
// init
@ -2312,8 +2315,8 @@ function Ace2Inner(editorInfo, cssManagers) {
if (isNaN(curLevel) || listType[0] === 'indent') {
return line;
} else if (curLevel === level) {
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
buildKeepRange(rep, builder, loc, (loc = [line, 0]));
buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
['start', position],
], rep.apool);
@ -2330,7 +2333,7 @@ function Ace2Inner(editorInfo, cssManagers) {
applyNumberList(lineNum, 1);
const cs = builder.toString();
if (!Changeset.isIdentity(cs)) {
if (!isIdentity(cs)) {
performDocumentApplyChangeset(cs);
}
@ -2618,7 +2621,7 @@ function Ace2Inner(editorInfo, cssManagers) {
// TODO: There appears to be a race condition or so.
const authorIds = new Set();
if (alineAttrs) {
for (const op of Changeset.deserializeOps(alineAttrs)) {
for (const op of deserializeOps(alineAttrs)) {
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
if (authorId) authorIds.add(authorId);
}
@ -3513,8 +3516,8 @@ function Ace2Inner(editorInfo, cssManagers) {
const oneEntry = createDomLineEntry('');
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
insertDomLines(null, [oneEntry.domInfo]);
rep.alines = Changeset.splitAttributionLines(
Changeset.makeAttribution('\n'), '\n');
rep.alines = splitAttributionLines(
makeAttribution('\n'), '\n');
bindTheEventHandlers();
});

View file

@ -26,7 +26,7 @@
const makeCSSManager = require('./cssmanager').makeCSSManager;
const domline = require('./domline').domline;
import AttribPool from './AttributePool';
const Changeset = require('./Changeset');
import {compose, deserializeOps, inverse, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, splitAttributionLines, splitTextLines, unpack} from './Changeset';
const attributes = require('./attributes');
const linestylefilter = require('./linestylefilter').linestylefilter;
const colorutils = require('./colorutils').colorutils;
@ -54,11 +54,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
currentRevision: clientVars.collab_client_vars.rev,
currentTime: clientVars.collab_client_vars.time,
currentLines:
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
currentDivs: null,
// to be filled in once the dom loads
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
alines: Changeset.splitAttributionLines(
alines: splitAttributionLines(
clientVars.collab_client_vars.initialAttributedText.attribs,
clientVars.collab_client_vars.initialAttributedText.text),
@ -121,7 +121,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
getActiveAuthors() {
const authorIds = new Set();
for (const aline of this.alines) {
for (const op of Changeset.deserializeOps(aline)) {
for (const op of deserializeOps(aline)) {
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
if (k !== 'author') continue;
if (v) authorIds.add(v);
@ -142,7 +142,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
const oldAlines = padContents.alines.slice();
try {
// must mutate attribution lines before text lines
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
mutateAttributionLines(changeset, padContents.alines, padContents.apool);
} catch (e) {
debugLog(e);
}
@ -164,7 +164,7 @@ 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) {
const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
const [op] = deserializeOps(unpack(changeset).ops);
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
}
@ -184,7 +184,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
goToLineNumber(lineChanged);
}
Changeset.mutateTextLines(changeset, padContents);
mutateTextLines(changeset, padContents);
padContents.currentRevision = revision;
padContents.currentTime += timeDelta * 1000;
@ -273,7 +273,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
let changeset = cs[0];
let timeDelta = path.times[0];
for (let i = 1; i < cs.length; i++) {
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
changeset = compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i];
}
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
@ -291,7 +291,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
let changeset = cs[0];
let timeDelta = path.times[0];
for (let i = 1; i < cs.length; i++) {
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
changeset = compose(changeset, cs[i], padContents.apool);
timeDelta += path.times[i];
}
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
@ -397,9 +397,9 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
// debugLog("adding changeset:", astart, aend);
const forwardcs =
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
const backwardcs =
Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
}
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
@ -409,13 +409,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
obj = obj.data;
if (obj.type === 'NEW_CHANGES') {
const changeset = Changeset.moveOpsToNewPool(
const changeset = moveOpsToNewPool(
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
let changesetBack = Changeset.inverse(
let changesetBack = inverse(
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
changesetBack = Changeset.moveOpsToNewPool(
changesetBack = moveOpsToNewPool(
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);

View file

@ -25,15 +25,16 @@
import AttributeMap from './AttributeMap';
import AttributePool from './AttributePool';
const Changeset = require('./Changeset');
import {applyToAText, checkRep, cloneAText, compose, deserializeOps, follow, identity, isIdentity, makeAText, moveOpsToNewPool, newLen, pack, prepareForWire, unpack} from './Changeset';
import {MergingOpAssembler} from "./MergingOpAssembler";
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
// latest official text from server
let baseAText = Changeset.makeAText('\n');
let baseAText = makeAText('\n');
// changes applied to baseText that have been submitted
let submittedChangeset = null;
// changes applied to submittedChangeset since it was prepared
let userChangeset = Changeset.identity(1);
let userChangeset = identity(1);
// is the changesetTracker enabled
let tracking = false;
// stack state flag so that when we change the rep we don't
@ -67,18 +68,18 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
return self = {
isTracking: () => tracking,
setBaseText: (text) => {
self.setBaseAttributedText(Changeset.makeAText(text), null);
self.setBaseAttributedText(makeAText(text), null);
},
setBaseAttributedText: (atext, apoolJsonObj) => {
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
tracking = true;
baseAText = Changeset.cloneAText(atext);
baseAText = cloneAText(atext);
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
baseAText.attribs = moveOpsToNewPool(baseAText.attribs, wireApool, apool);
}
submittedChangeset = null;
userChangeset = Changeset.identity(atext.text.length);
userChangeset = identity(atext.text.length);
applyingNonUserChanges = true;
try {
callbacks.setDocumentAttributedText(atext);
@ -90,8 +91,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
composeUserChangeset: (c) => {
if (!tracking) return;
if (applyingNonUserChanges) return;
if (Changeset.isIdentity(c)) return;
userChangeset = Changeset.compose(userChangeset, c, apool);
if (isIdentity(c)) return;
userChangeset = compose(userChangeset, c, apool);
setChangeCallbackTimeout();
},
@ -101,23 +102,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
if (apoolJsonObj) {
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
c = moveOpsToNewPool(c, wireApool, apool);
}
baseAText = Changeset.applyToAText(c, baseAText, apool);
baseAText = applyToAText(c, baseAText, apool);
let c2 = c;
if (submittedChangeset) {
const oldSubmittedChangeset = submittedChangeset;
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
submittedChangeset = follow(c, oldSubmittedChangeset, false, apool);
c2 = follow(oldSubmittedChangeset, c, true, apool);
}
const preferInsertingAfterUserChanges = true;
const oldUserChangeset = userChangeset;
userChangeset = Changeset.follow(
userChangeset = follow(
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
const postChange = Changeset.follow(
const postChange = follow(
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
@ -136,17 +137,17 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
if (submittedChangeset) {
// submission must have been canceled, prepare new changeset
// that includes old submittedChangeset
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
toSubmit = compose(submittedChangeset, userChangeset, apool);
} else {
// Get my authorID
const authorId = parent.parent.pad.myUserInfo.userId;
// 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 assem = Changeset.mergingOpAssembler();
const cs = unpack(userChangeset);
const assem = new MergingOpAssembler();
for (const op of Changeset.deserializeOps(cs.ops)) {
for (const op of deserializeOps(cs.ops)) {
if (op.opcode === '+') {
const attribs = AttributeMap.fromString(op.attribs, apool);
const oldAuthorId = attribs.get('author');
@ -158,23 +159,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
assem.append(op);
}
assem.endDocument();
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
Changeset.checkRep(userChangeset);
userChangeset = pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
checkRep(userChangeset);
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
if (isIdentity(userChangeset)) toSubmit = null;
else toSubmit = userChangeset;
}
let cs = null;
if (toSubmit) {
submittedChangeset = toSubmit;
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
userChangeset = identity(newLen(toSubmit));
cs = toSubmit;
}
let wireApool = null;
if (cs) {
const forWire = Changeset.prepareForWire(cs, apool);
const forWire = prepareForWire(cs, apool);
wireApool = forWire.pool.toJsonable();
cs = forWire.translated;
}
@ -191,13 +192,13 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
}
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
baseAText = applyToAText(submittedChangeset, baseAText, apool);
submittedChangeset = null;
},
setUserChangeNotificationCallback: (callback) => {
changeCallback = callback;
},
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
hasUncommittedChanges: () => !!(submittedChangeset || (!isIdentity(userChangeset))),
};
};

View file

@ -1,4 +1,5 @@
// @ts-nocheck
'use strict';
/**
* This code is mostly from the old Etherpad. Please help us to comment this code.
@ -9,6 +10,8 @@
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
// %APPJET%: import("etherpad.admin.plugins");
import Op from "./Op";
/**
* Copyright 2009 Google Inc.
*
@ -28,8 +31,9 @@
const _MAX_LIST_LEVEL = 16;
import AttributeMap from './AttributeMap';
const UNorm = require('unorm');
const Changeset = require('./Changeset');
import UNorm from 'unorm';
import {subattribution} from './Changeset';
import {SmartOpAssembler} from "./SmartOpAssembler";
const hooks = require('./pluginfw/hooks');
const sanitizeUnicode = (s) => UNorm.nfc(s);
@ -84,14 +88,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const textArray = [];
const attribsArray = [];
let attribsBuilder = null;
const op = new Changeset.Op('+');
const op = new Op('+');
const self = {
length: () => textArray.length,
atColumnZero: () => textArray[textArray.length - 1] === '',
startNew: () => {
textArray.push('');
self.flush(true);
attribsBuilder = Changeset.smartOpAssembler();
attribsBuilder = new SmartOpAssembler();
},
textOfLine: (i) => textArray[i],
appendText: (txt, attrString = '') => {
@ -654,8 +658,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
const lengthToTake = lineLimit;
newStrings.push(oldString.substring(0, lengthToTake));
oldString = oldString.substring(lengthToTake);
newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
newAttribStrings.push(subattribution(oldAttribString, 0, lengthToTake));
oldAttribString = subattribution(oldAttribString, lengthToTake);
}
if (oldString.length > 0) {
newStrings.push(oldString);

View file

@ -31,12 +31,13 @@
// requires: plugins
// requires: undefined
const Changeset = require('./Changeset');
const attributes = require('./attributes');
import {deserializeOps} from './Changeset';
import attributes from './attributes';
const hooks = require('./pluginfw/hooks');
const linestylefilter = {};
const AttributeManager = require('./AttributeManager');
import padutils from './pad_utils'
import Op from "./Op";
linestylefilter.ATTRIB_CLASSES = {
bold: 'tag:b',
@ -99,12 +100,12 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
return classes.substring(1);
};
const attrOps = Changeset.deserializeOps(aline);
const attrOps = deserializeOps(aline);
let attrOpsNext = attrOps.next();
let nextOp, nextOpClasses;
const goNextOp = () => {
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
nextOp = attrOpsNext.done ? new Op() : attrOpsNext.value;
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
};

View file

@ -0,0 +1,6 @@
export type ChangeSet = {
oldLen: number,
newLen: number,
ops: string
charBank: string
}

View file

@ -0,0 +1,7 @@
import {Attribute} from "./Attribute";
import AttributePool from "../AttributePool";
export type ChangeSetBuilder = {
remove: (start: number, end?: number)=>void,
keep: (start: number, end?: number, attribs?: Attribute[], pool?: AttributePool)=>void
}

View file

@ -23,7 +23,7 @@
* limitations under the License.
*/
const Changeset = require('./Changeset');
import {characterRangeFollow, compose, follow, isIdentity, unpack} from './Changeset';
const _ = require('./underscore');
const undoModule = (() => {
@ -62,7 +62,7 @@ const undoModule = (() => {
const idx = stackElements.length - 1;
if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
stackElements[idx].changeset =
Changeset.compose(stackElements[idx].changeset, cs, getAPool());
compose(stackElements[idx].changeset, cs, getAPool());
} else {
stackElements.push(
{
@ -83,10 +83,10 @@ const undoModule = (() => {
if (un.backset) {
const excs = ex.changeset;
const unbs = un.backset;
un.backset = Changeset.follow(excs, un.backset, false, getAPool());
ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool());
un.backset = follow(excs, un.backset, false, getAPool());
ex.changeset = follow(unbs, ex.changeset, true, getAPool());
if ((typeof un.selStart) === 'number') {
const newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd);
const newSel = characterRangeFollow(excs, un.selStart, un.selEnd);
un.selStart = newSel[0];
un.selEnd = newSel[1];
if (un.selStart === un.selEnd) {
@ -98,7 +98,7 @@ const undoModule = (() => {
stackElements[idx] = un;
if (idx >= 2 && stackElements[idx - 2].elementType === EXTERNAL_CHANGE) {
ex.changeset =
Changeset.compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
stackElements.splice(idx - 2, 1);
idx--;
}
@ -154,7 +154,7 @@ const undoModule = (() => {
return count;
};
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(Changeset.unpack(cs).ops, opcode);
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(unpack(cs).ops, opcode);
const _mergeChangesets = (cs1, cs2) => {
if (!cs1) return cs2;
@ -171,14 +171,14 @@ const undoModule = (() => {
const minusCount1 = _opcodeOccurrences(cs1, '-');
const minusCount2 = _opcodeOccurrences(cs2, '-');
if (plusCount1 === 1 && plusCount2 === 1 && minusCount1 === 0 && minusCount2 === 0) {
const merge = Changeset.compose(cs1, cs2, getAPool());
const merge = compose(cs1, cs2, getAPool()!);
const plusCount3 = _opcodeOccurrences(merge, '+');
const minusCount3 = _opcodeOccurrences(merge, '-');
if (plusCount3 === 1 && minusCount3 === 0) {
return merge;
}
} else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
const merge = Changeset.compose(cs1, cs2, getAPool());
const merge = compose(cs1, cs2, getAPool()!);
const plusCount3 = _opcodeOccurrences(merge, '+');
const minusCount3 = _opcodeOccurrences(merge, '-');
if (plusCount3 === 0 && minusCount3 === 1) {
@ -199,7 +199,7 @@ const undoModule = (() => {
}
};
if ((!event.backset) || Changeset.isIdentity(event.backset)) {
if ((!event.backset) || isIdentity(event.backset)) {
applySelectionToTop();
} else {
let merged = false;
@ -227,7 +227,7 @@ const undoModule = (() => {
};
const reportExternalChange = (changeset) => {
if (changeset && !Changeset.isIdentity(changeset)) {
if (changeset && !isIdentity(changeset)) {
stack.pushExternalChange(changeset);
}
};