mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-22 16:36:15 -04:00
Feat/changeset ts (#6594)
* Migrated changeset * Added more tests. * Fixed test scopes
This commit is contained in:
parent
3dae23a1e5
commit
28e04bdf71
37 changed files with 2540 additions and 1310 deletions
|
@ -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
108
src/static/js/Builder.ts
Normal 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
|
@ -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();
|
||||
|
|
73
src/static/js/MergingOpAssembler.ts
Normal file
73
src/static/js/MergingOpAssembler.ts
Normal 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
78
src/static/js/Op.ts
Normal 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);
|
||||
}
|
||||
}
|
21
src/static/js/OpAssembler.ts
Normal file
21
src/static/js/OpAssembler.ts
Normal 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
47
src/static/js/OpIter.ts
Normal 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;
|
||||
}
|
||||
}
|
115
src/static/js/SmartOpAssembler.ts
Normal file
115
src/static/js/SmartOpAssembler.ts
Normal 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;
|
||||
}
|
18
src/static/js/StringAssembler.ts
Normal file
18
src/static/js/StringAssembler.ts
Normal 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
|
||||
}
|
||||
}
|
54
src/static/js/StringIterator.ts
Normal file
54
src/static/js/StringIterator.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
348
src/static/js/TextLinesMutator.ts
Normal file
348
src/static/js/TextLinesMutator.ts
Normal 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
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
|
|
6
src/static/js/types/ChangeSet.ts
Normal file
6
src/static/js/types/ChangeSet.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type ChangeSet = {
|
||||
oldLen: number,
|
||||
newLen: number,
|
||||
ops: string
|
||||
charBank: string
|
||||
}
|
7
src/static/js/types/ChangeSetBuilder.ts
Normal file
7
src/static/js/types/ChangeSetBuilder.ts
Normal 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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue