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

@ -19,8 +19,10 @@
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
import {deserializeOps} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import {Builder} from "../../static/js/Builder";
import {Attribute} from "../../static/js/types/Attribute";
const CustomError = require('../utils/customError');
const padManager = require('./PadManager');
const padMessageHandler = require('../handler/PadMessageHandler');
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
const oldText = pad.text();
atext.text += '\n';
const eachAttribRun = (attribs: string[], func:Function) => {
const eachAttribRun = (attribs: string, func:Function) => {
let textIndex = 0;
const newTextStart = 0;
const newTextEnd = atext.text.length;
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);
@ -577,10 +579,10 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
};
// create a new changeset with a helper builder object
const builder = Changeset.builder(oldText.length);
const builder = new Builder(oldText.length);
// assemble each line into the builder
eachAttribRun(atext.attribs, (start: number, end: number, attribs:string[]) => {
eachAttribRun(atext.attribs, (start: number, end: number, attribs:Attribute[]) => {
builder.insert(atext.text.substring(start, end), attribs);
});

View file

@ -8,7 +8,7 @@ import {MapArrayType} from "../types/MapType";
*/
import AttributeMap from '../../static/js/AttributeMap';
const Changeset = require('../../static/js/Changeset');
import {applyToAText, checkRep, copyAText, deserializeOps, makeAText, makeSplice, opsFromAText, pack, unpack} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import AttributePool from '../../static/js/AttributePool';
const Stream = require('../utils/Stream');
@ -24,6 +24,7 @@ const readOnlyManager = require('./ReadOnlyManager');
const randomString = require('../utils/randomstring');
const hooks = require('../../static/js/pluginfw/hooks');
import pad_utils from "../../static/js/pad_utils";
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
const promises = require('../utils/promises');
/**
@ -56,7 +57,7 @@ class Pad {
*/
constructor(id:string, database = db) {
this.db = database;
this.atext = Changeset.makeAText('\n');
this.atext = makeAText('\n');
this.pool = new AttributePool();
this.head = -1;
this.chatHead = -1;
@ -93,13 +94,13 @@ class Pad {
* @param {String} authorId The id of the author
* @return {Promise<number|string>}
*/
async appendRevision(aChangeset:AChangeSet, authorId = '') {
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
async appendRevision(aChangeset:string, authorId = '') {
const newAText = applyToAText(aChangeset, this.atext, this.pool);
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
this.head !== -1) {
return this.head;
}
Changeset.copyAText(newAText, this.atext);
copyAText(newAText, this.atext);
const newRev = ++this.head;
@ -215,7 +216,7 @@ class Pad {
]);
const apool = this.apool();
let atext = keyAText;
for (const cs of changesets) atext = Changeset.applyToAText(cs, atext, apool);
for (const cs of changesets) atext = applyToAText(cs, atext, apool);
return atext;
}
@ -293,7 +294,7 @@ class Pad {
(!ins && start > 0 && orig[start - 1] === '\n');
if (!willEndWithNewline) ins += '\n';
if (ndel === 0 && ins.length === 0) return;
const changeset = Changeset.makeSplice(orig, start, ndel, ins);
const changeset = makeSplice(orig, start, ndel, ins);
await this.appendRevision(changeset, authorId);
}
@ -393,7 +394,7 @@ class Pad {
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
text = exports.cleanText(context.content);
}
const firstChangeset = Changeset.makeSplice('\n', 0, 0, text);
const firstChangeset = makeSplice('\n', 0, 0, text);
await this.appendRevision(firstChangeset, authorId);
}
await hooks.aCallAll('padLoad', {pad: this});
@ -520,8 +521,8 @@ class Pad {
const oldAText = this.atext;
// based on Changeset.makeSplice
const assem = Changeset.smartOpAssembler();
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
const assem = new SmartOpAssembler();
for (const op of opsFromAText(oldAText)) assem.append(op);
assem.endDocument();
// although we have instantiated the dstPad with '\n', an additional '\n' is
@ -533,7 +534,7 @@ class Pad {
// create a changeset that removes the previous text and add the newText with
// all atributes present on the source pad
const changeset = Changeset.pack(oldLength, newLength, assem.toString(), newText);
const changeset = pack(oldLength, newLength, assem.toString(), newText);
dstPad.appendRevision(changeset, authorId);
await hooks.aCallAll('padCopy', {
@ -706,7 +707,7 @@ class Pad {
}
})
.batch(100).buffer(99);
let atext = Changeset.makeAText('\n');
let atext = makeAText('\n');
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
try {
assert(authorId != null);
@ -717,10 +718,10 @@ class Pad {
assert(timestamp > 0);
assert(changeset != null);
assert.equal(typeof changeset, 'string');
Changeset.checkRep(changeset);
const unpacked = Changeset.unpack(changeset);
checkRep(changeset);
const unpacked = unpack(changeset);
let text = atext.text;
for (const op of Changeset.deserializeOps(unpacked.ops)) {
for (const op of deserializeOps(unpacked.ops)) {
if (['=', '-'].includes(op.opcode)) {
assert(text.length >= op.chars);
const consumed = text.slice(0, op.chars);
@ -731,7 +732,7 @@ class Pad {
}
assert.equal(op.attribs, AttributeMap.fromString(op.attribs, pool).toString());
}
atext = Changeset.applyToAText(changeset, atext, pool);
atext = applyToAText(changeset, atext, pool);
if (isKeyRev) assert.deepEqual(keyAText, atext);
} catch (err:any) {
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;

View file

@ -23,7 +23,7 @@ import {MapArrayType} from "../types/MapType";
import AttributeMap from '../../static/js/AttributeMap';
const padManager = require('../db/PadManager');
const Changeset = require('../../static/js/Changeset');
import {checkRep, cloneAText, compose, deserializeOps, follow, identity, inverse, makeAText, makeSplice, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, prepareForWire, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
import ChatMessage from '../../static/js/ChatMessage';
import AttributePool from '../../static/js/AttributePool';
const AttributeManager = require('../../static/js/AttributeManager');
@ -44,6 +44,7 @@ import {ChangesetRequest, PadUserInfo, SocketClientRequest} from "../types/Socke
import {APool, AText, PadAuthor, PadType} from "../types/PadType";
import {ChangeSet} from "../types/ChangeSet";
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
import {Builder} from "../../static/js/Builder";
const webaccess = require('../hooks/express/webaccess');
const { checkValidRev } = require('../utils/checkValidRev');
@ -594,10 +595,10 @@ const handleUserChanges = async (socket:any, message: {
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
// Verify that the changeset has valid syntax and is in canonical form
Changeset.checkRep(changeset);
checkRep(changeset);
// Validate all added 'author' attribs to be the same value as the current user
for (const op of Changeset.deserializeOps(Changeset.unpack(changeset).ops)) {
for (const op of deserializeOps(unpack(changeset).ops)) {
// + can add text with attribs
// = can change or add attribs
// - can have attribs, but they are discarded and don't show up in the attribs -
@ -616,7 +617,7 @@ const handleUserChanges = async (socket:any, message: {
// ex. adoptChangesetAttribs
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
let rebasedChangeset = Changeset.moveOpsToNewPool(changeset, wireApool, pad.pool);
let rebasedChangeset = moveOpsToNewPool(changeset, wireApool, pad.pool);
// ex. applyUserChanges
let r = baseRev;
@ -629,21 +630,21 @@ const handleUserChanges = async (socket:any, message: {
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
if (changeset === c && thisSession.author === authorId) {
// Assume this is a retransmission of an already applied changeset.
rebasedChangeset = Changeset.identity(Changeset.unpack(changeset).oldLen);
rebasedChangeset = identity(unpack(changeset).oldLen);
}
// At this point, both "c" (from the pad) and "changeset" (from the
// client) are relative to revision r - 1. The follow function
// rebases "changeset" so that it is relative to revision r
// and can be applied after "c".
rebasedChangeset = Changeset.follow(c, rebasedChangeset, false, pad.pool);
rebasedChangeset = follow(c, rebasedChangeset, false, pad.pool);
}
const prevText = pad.text();
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
if (oldLen(rebasedChangeset) !== prevText.length) {
throw new Error(
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
`${Changeset.oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
`${oldLen(rebasedChangeset)} to document of length ${prevText.length}`);
}
const newRev = await pad.appendRevision(rebasedChangeset, thisSession.author);
@ -658,7 +659,7 @@ const handleUserChanges = async (socket:any, message: {
// Make sure the pad always ends with an empty line.
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
const nlChangeset = Changeset.makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
const nlChangeset = makeSplice(pad.text(), pad.text().length - 1, 0, '\n');
await pad.appendRevision(nlChangeset, thisSession.author);
}
@ -713,7 +714,7 @@ exports.updatePadClients = async (pad: PadType) => {
const revChangeset = revision.changeset;
const currentTime = revision.meta.timestamp;
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
const forWire = prepareForWire(revChangeset, pad.pool);
const msg = {
type: 'COLLABROOM',
data: {
@ -748,7 +749,7 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
// that aren't at the start of a line
const badMarkers = [];
let offset = 0;
for (const op of Changeset.deserializeOps(atext.attribs)) {
for (const op of deserializeOps(atext.attribs)) {
const attribs = AttributeMap.fromString(op.attribs, apool);
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
if (hasMarker) {
@ -770,7 +771,7 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
// create changeset that removes these bad markers
offset = 0;
const builder = Changeset.builder(text.length);
const builder = new Builder(text.length);
badMarkers.forEach((pos) => {
builder.keepText(text.substring(offset, pos));
@ -905,7 +906,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
// return pending changesets
for (const r of revisionsNeeded) {
const forWire = Changeset.prepareForWire(changesets[r].changeset, pad.pool);
const forWire = prepareForWire(changesets[r].changeset, pad.pool);
const wireMsg = {type: 'COLLABROOM',
data: {type: 'CLIENT_RECONNECT',
headRev: pad.getHeadRevisionNumber(),
@ -930,8 +931,8 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
let apool;
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
try {
atext = Changeset.cloneAText(pad.atext);
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
atext = cloneAText(pad.atext);
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
apool = attribsForWire.pool.toJsonable();
atext.attribs = attribsForWire.translated;
} catch (e:any) {
@ -1167,13 +1168,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
const backwards = Changeset.inverse(forwards, lines.textlines, lines.alines, pad.apool());
const backwards = inverse(forwards, lines.textlines, lines.alines, pad.apool());
Changeset.mutateAttributionLines(forwards, lines.alines, pad.apool());
Changeset.mutateTextLines(forwards, lines.textlines);
mutateAttributionLines(forwards, lines.alines, pad.apool());
mutateTextLines(forwards, lines.textlines);
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
const t2 = revisionDate[compositeEnd - 1];
@ -1199,12 +1200,12 @@ const getPadLines = async (pad: PadType, revNum: number) => {
if (revNum >= 0) {
atext = await pad.getInternalRevisionAText(revNum);
} else {
atext = Changeset.makeAText('\n');
atext = makeAText('\n');
}
return {
textlines: Changeset.splitTextLines(atext.text),
alines: Changeset.splitAttributionLines(atext.attribs, atext.text),
textlines: splitTextLines(atext.text),
alines: splitAttributionLines(atext.attribs, atext.text),
};
};
@ -1239,7 +1240,7 @@ const composePadChangesets = async (pad: PadType, startNum: number, endNum: numb
for (r = startNum + 1; r < endNum; r++) {
const cs = changesets[r];
changeset = Changeset.compose(changeset, cs, pool);
changeset = compose(changeset as string, cs as string, pool);
}
return changeset;
} catch (e) {

View file

@ -1,10 +1,11 @@
import {MapArrayType} from "./MapType";
import AttributePool from "../../static/js/AttributePool";
export type PadType = {
id: string,
apool: ()=>APool,
apool: ()=>AttributePool,
atext: AText,
pool: APool,
pool: AttributePool,
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
getRevisionAuthor: (rev: number)=>Promise<string>,

View file

@ -21,7 +21,7 @@
import AttributeMap from '../../static/js/AttributeMap';
import AttributePool from "../../static/js/AttributePool";
const Changeset = require('../../static/js/Changeset');
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
const { checkValidRev } = require('./checkValidRev');
/*
@ -31,7 +31,7 @@ exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any;
const _analyzeLine = exports._analyzeLine;
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const apool = pad.pool;
const pieces = [];
@ -52,14 +52,14 @@ type LineModel = {
[id:string]:string|number|LineModel
}
exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) => {
exports._analyzeLine = (text:string, aline: string, apool: AttributePool) => {
const line: LineModel = {};
// identify list
let lineMarker = 0;
line.listLevel = 0;
if (aline) {
const [op] = Changeset.deserializeOps(aline);
const [op] = deserializeOps(aline);
if (op != null) {
const attribs = AttributeMap.fromString(op.attribs, apool);
let listType = attribs.get('list');
@ -79,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) =>
}
if (lineMarker) {
line.text = text.substring(1);
line.aline = Changeset.subattribution(aline, 1);
line.aline = subattribution(aline, 1);
} else {
line.text = text;
line.aline = aline;

View file

@ -18,7 +18,7 @@ import {MapArrayType} from "../types/MapType";
* limitations under the License.
*/
const Changeset = require('../../static/js/Changeset');
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _ = require('underscore');
@ -28,6 +28,8 @@ const eejs = require('../eejs');
const _analyzeLine = require('./ExportHelper')._analyzeLine;
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
import padutils from "../../static/js/pad_utils";
import {StringIterator} from "../../static/js/StringIterator";
import {StringAssembler} from "../../static/js/StringAssembler";
const getPadHTML = async (pad: PadType, revNum: string) => {
let atext = pad.atext;
@ -44,7 +46,7 @@ const getPadHTML = async (pad: PadType, revNum: string) => {
const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
css += '<style>\n';
for (const a of Object.keys(apool.numToAttrib)) {
// @ts-ignore
const attr = apool.numToAttrib[a];
// skip non author attributes
@ -115,6 +118,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
// see hook exportHtmlAdditionalTagsWithData
attrib = propName;
}
// @ts-ignore
const propTrueNum = apool.putAttrib(attrib, true);
if (propTrueNum >= 0) {
anumMap[propTrueNum] = i;
@ -127,8 +131,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
const taker = Changeset.stringIterator(text);
const assem = Changeset.stringAssembler();
const taker = new StringIterator(text);
const assem = new StringAssembler();
const openTags:string[] = [];
const getSpanClassFor = (i: string) => {
@ -204,7 +208,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
return;
}
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
// @ts-ignore
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
idx += numChars;
// this iterates over every op string and decides which tags to open or to close

View file

@ -22,7 +22,9 @@
import {AText, PadType} from "../types/PadType";
import {MapType} from "../types/MapType";
const Changeset = require('../../static/js/Changeset');
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
import {StringIterator} from "../../static/js/StringIterator";
import {StringAssembler} from "../../static/js/StringAssembler";
const attributes = require('../../static/js/attributes');
const padManager = require('../db/PadManager');
const _analyzeLine = require('./ExportHelper')._analyzeLine;
@ -45,13 +47,14 @@ const getPadTXT = async (pad: PadType, revNum: string) => {
const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
const apool = pad.apool();
const textLines = atext.text.slice(0, -1).split('\n');
const attribLines = Changeset.splitAttributionLines(atext.attribs, atext.text);
const attribLines = splitAttributionLines(atext.attribs, atext.text);
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
const anumMap: MapType = {};
const css = '';
props.forEach((propName, i) => {
// @ts-ignore
const propTrueNum = apool.putAttrib([propName, true], true);
if (propTrueNum >= 0) {
anumMap[propTrueNum] = i;
@ -69,8 +72,8 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
// becomes
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
const taker = Changeset.stringIterator(text);
const assem = Changeset.stringAssembler();
const taker = new StringIterator(text);
const assem = new StringAssembler();
let idx = 0;
@ -79,7 +82,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
return;
}
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
idx += numChars;
for (const o of ops) {

View file

@ -16,10 +16,11 @@
*/
import log4js from 'log4js';
const Changeset = require('../../static/js/Changeset');
import {deserializeOps} from '../../static/js/Changeset';
const contentcollector = require('../../static/js/contentcollector');
import jsdom from 'jsdom';
import {PadType} from "../types/PadType";
import {Builder} from "../../static/js/Builder";
const apiLogger = log4js.getLogger('ImportHtml');
let processor:any;
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
// create a new changeset with a helper builder object
const builder = Changeset.builder(1);
const builder = new Builder(1);
// assemble each line into the builder
let textIndex = 0;
const newTextStart = 0;
const newTextEnd = newText.length;
for (const op of Changeset.deserializeOps(newAttribs)) {
for (const op of deserializeOps(newAttribs)) {
const nextIndex = textIndex + op.chars;
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
const start = Math.max(newTextStart, textIndex);

View file

@ -4,7 +4,12 @@ import {PadAuthor, PadType} from "../types/PadType";
import {MapArrayType} from "../types/MapType";
import AttributeMap from '../../static/js/AttributeMap';
const Changeset = require('../../static/js/Changeset');
import {applyToAText, checkRep, compose, deserializeOps, pack, splitAttributionLines, splitTextLines, unpack} from '../../static/js/Changeset';
import {Builder} from "../../static/js/Builder";
import {OpAssembler} from "../../static/js/OpAssembler";
import {numToString} from "../../static/js/ChangesetUtils";
import Op from "../../static/js/Op";
import {StringAssembler} from "../../static/js/StringAssembler";
const attributes = require('../../static/js/attributes');
const exportHtml = require('./ExportHtml');
@ -33,7 +38,7 @@ class PadDiff {
}
_isClearAuthorship(changeset: any){
// unpack
const unpacked = Changeset.unpack(changeset);
const unpacked = unpack(changeset);
// check if there is nothing in the charBank
if (unpacked.charBank !== '') {
@ -45,7 +50,7 @@ class PadDiff {
return false;
}
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
const [clearOperator, anotherOp] = deserializeOps(unpacked.ops);
// check if there is only one operator
if (anotherOp != null) return false;
@ -78,7 +83,7 @@ class PadDiff {
const atext = await this._pad.getInternalRevisionAText(rev);
// build clearAuthorship changeset
const builder = Changeset.builder(atext.text.length);
const builder = new Builder(atext.text.length);
builder.keepText(atext.text, [['author', '']], this._pad.pool);
const changeset = builder.toString();
@ -93,7 +98,7 @@ class PadDiff {
const changeset = await this._createClearAuthorship(rev);
// apply the clearAuthorship changeset
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
const newAText = applyToAText(changeset, atext, this._pad.pool);
return newAText;
}
@ -157,7 +162,7 @@ class PadDiff {
if (superChangeset == null) {
superChangeset = changeset;
} else {
superChangeset = Changeset.compose(superChangeset, changeset, this._pad.pool);
superChangeset = compose(superChangeset, changeset, this._pad.pool);
}
}
@ -171,10 +176,10 @@ class PadDiff {
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
// apply the superChangeset, which includes all addings
atext = Changeset.applyToAText(superChangeset, atext, this._pad.pool);
atext = applyToAText(superChangeset, atext, this._pad.pool);
// apply the deletionChangeset, which adds a deletions
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
atext = applyToAText(deletionChangeset, atext, this._pad.pool);
}
return atext;
@ -209,22 +214,22 @@ class PadDiff {
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
// unpack
const unpacked = Changeset.unpack(changeset);
const unpacked = unpack(changeset);
const assem = Changeset.opAssembler();
const assem = new OpAssembler();
// create deleted attribs
const authorAttrib = apool.putAttrib(['author', author || '']);
const deletedAttrib = apool.putAttrib(['removed', true]);
const attribs = `*${Changeset.numToString(authorAttrib)}*${Changeset.numToString(deletedAttrib)}`;
const attribs = `*${numToString(authorAttrib)}*${numToString(deletedAttrib)}`;
for (const operator of Changeset.deserializeOps(unpacked.ops)) {
for (const operator of deserializeOps(unpacked.ops)) {
if (operator.opcode === '-') {
// this is a delete operator, extend it with the author
operator.attribs = attribs;
} else if (operator.opcode === '=' && operator.attribs) {
// this is operator changes only attributes, let's mark which author did that
operator.attribs += `*${Changeset.numToString(authorAttrib)}`;
operator.attribs += `*${numToString(authorAttrib)}`;
}
// append the new operator to our assembler
@ -232,26 +237,31 @@ class PadDiff {
}
// return the modified changeset
return Changeset.pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
return pack(unpacked.oldLen, unpacked.newLen, assem.toString(), unpacked.charBank);
}
_createDeletionChangeset(cs: any, startAText: any, apool: any){
const lines = Changeset.splitTextLines(startAText.text);
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
const lines = splitTextLines(startAText.text);
const alines = splitAttributionLines(startAText.attribs, startAText.text);
// lines and alines are what the exports is meant to apply to.
// They may be arrays or objects with .get(i) and .length methods.
// They include final newlines on lines.
const linesGet = (idx: number) => {
// @ts-ignore
if (lines.get) {
// @ts-ignore
return lines.get(idx);
} else {
// @ts-ignore
return lines[idx];
}
};
const aLinesGet = (idx: number) => {
// @ts-ignore
if (alines.get) {
// @ts-ignore
return alines.get(idx);
} else {
return alines[idx];
@ -263,14 +273,14 @@ class PadDiff {
let curLineOps: { next: () => any; } | null = null;
let curLineOpsNext: { done: any; value: any; } | null = null;
let curLineOpsLine: number;
let curLineNextOp = new Changeset.Op('+');
let curLineNextOp = new Op('+');
const unpacked = Changeset.unpack(cs);
const builder = Changeset.builder(unpacked.newLen);
const unpacked = unpack(cs);
const builder = new Builder(unpacked.newLen);
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
if (!curLineOps || curLineOpsLine !== curLine) {
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOps = deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps!.next();
curLineOpsLine = curLine;
let indexIntoLine = 0;
@ -291,13 +301,13 @@ class PadDiff {
curChar = 0;
curLineOpsLine = curLine;
curLineNextOp.chars = 0;
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
curLineOps = deserializeOps(aLinesGet(curLine));
curLineOpsNext = curLineOps!.next();
}
if (!curLineNextOp.chars) {
if (curLineOpsNext!.done) {
curLineNextOp = new Changeset.Op();
curLineNextOp = new Op();
} else {
curLineNextOp = curLineOpsNext!.value;
curLineOpsNext = curLineOps!.next();
@ -332,7 +342,7 @@ class PadDiff {
const nextText = (numChars: number) => {
let len = 0;
const assem = Changeset.stringAssembler();
const assem = new StringAssembler();
const firstString = linesGet(curLine).substring(curChar);
len += firstString.length;
assem.append(firstString);
@ -360,7 +370,7 @@ class PadDiff {
};
};
for (const csOp of Changeset.deserializeOps(unpacked.ops)) {
for (const csOp of deserializeOps(unpacked.ops)) {
if (csOp.opcode === '=') {
const textBank = nextText(csOp.chars);
@ -442,7 +452,7 @@ class PadDiff {
}
}
return Changeset.checkRep(builder.toString());
return checkRep(builder.toString());
}
}
@ -450,6 +460,7 @@ class PadDiff {
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
// it adds deletions and attribute changes to the atext.
// @ts-ignore
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
};

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);
}
};

View file

@ -0,0 +1,220 @@
import AttributePool from "../../static/js/AttributePool";
import { Attribute } from "../../static/js/types/Attribute";
import {StringAssembler} from "../../static/js/StringAssembler";
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
import Op from "../../static/js/Op";
import {numToString} from "../../static/js/ChangesetUtils";
import {checkRep, pack} from "../../static/js/Changeset";
export const poolOrArray = (attribs: any) => {
if (attribs.getAttrib) {
return attribs; // it's already an attrib pool
} else {
// assume it's an array of attrib strings to be split and added
const p = new AttributePool();
attribs.forEach((kv: { split: (arg0: string) => Attribute; }) => {
p.putAttrib(kv.split(','));
});
return p;
}
};
const randInt = (maxValue: number) => Math.floor(Math.random() * maxValue);
const randomInlineString = (len: number) => {
const assem = new StringAssembler();
for (let i = 0; i < len; i++) {
assem.append(String.fromCharCode(randInt(26) + 97));
}
return assem.toString();
};
export const randomMultiline = (approxMaxLines: number, approxMaxCols: number) => {
const numParts = randInt(approxMaxLines * 2) + 1;
const txt = new StringAssembler();
txt.append(randInt(2) ? '\n' : '');
for (let i = 0; i < numParts; i++) {
if ((i % 2) === 0) {
if (randInt(10)) {
txt.append(randomInlineString(randInt(approxMaxCols) + 1));
} else {
txt.append('\n');
}
} else {
txt.append('\n');
}
}
return txt.toString();
};
const randomTwoPropAttribs = (opcode: "" | "=" | "+" | "-") => {
// assumes attrib pool like ['apple,','apple,true','banana,','banana,true']
if (opcode === '-' || randInt(3)) {
return '';
} else if (randInt(3)) { // eslint-disable-line no-dupe-else-if
if (opcode === '+' || randInt(2)) {
return `*${numToString(randInt(2) * 2 + 1)}`;
} else {
return `*${numToString(randInt(2) * 2)}`;
}
} else if (opcode === '+' || randInt(4) === 0) {
return '*1*3';
} else {
return ['*0*2', '*0*3', '*1*2'][randInt(3)];
}
};
const randomStringOperation = (numCharsLeft: number) => {
let result;
switch (randInt(11)) {
case 0:
{
// insert char
result = {
insert: randomInlineString(1),
};
break;
}
case 1:
{
// delete char
result = {
remove: 1,
};
break;
}
case 2:
{
// skip char
result = {
skip: 1,
};
break;
}
case 3:
{
// insert small
result = {
insert: randomInlineString(randInt(4) + 1),
};
break;
}
case 4:
{
// delete small
result = {
remove: randInt(4) + 1,
};
break;
}
case 5:
{
// skip small
result = {
skip: randInt(4) + 1,
};
break;
}
case 6:
{
// insert multiline;
result = {
insert: randomMultiline(5, 20),
};
break;
}
case 7:
{
// delete multiline
result = {
remove: Math.round(numCharsLeft * Math.random() * Math.random()),
};
break;
}
case 8:
{
// skip multiline
result = {
skip: Math.round(numCharsLeft * Math.random() * Math.random()),
};
break;
}
case 9:
{
// delete to end
result = {
remove: numCharsLeft,
};
break;
}
case 10:
{
// skip to end
result = {
skip: numCharsLeft,
};
break;
}
}
const maxOrig = numCharsLeft - 1;
if ('remove' in result!) {
result.remove = Math.min(result.remove, maxOrig);
} else if ('skip' in result!) {
result.skip = Math.min(result.skip, maxOrig);
}
return result;
};
export const randomTestChangeset = (origText: string, withAttribs?: any) => {
const charBank = new StringAssembler();
let textLeft = origText; // always keep final newline
const outTextAssem = new StringAssembler();
const opAssem = new SmartOpAssembler();
const oldLen = origText.length;
const nextOp = new Op();
const appendMultilineOp = (opcode: "" | "=" | "+" | "-", txt: string) => {
nextOp.opcode = opcode;
if (withAttribs) {
nextOp.attribs = randomTwoPropAttribs(opcode);
}
txt.replace(/\n|[^\n]+/g, (t) => {
if (t === '\n') {
nextOp.chars = 1;
nextOp.lines = 1;
opAssem.append(nextOp);
} else {
nextOp.chars = t.length;
nextOp.lines = 0;
opAssem.append(nextOp);
}
return '';
});
};
const doOp = () => {
const o = randomStringOperation(textLeft.length);
if (o!.insert) {
const txt = o!.insert;
charBank.append(txt);
outTextAssem.append(txt);
appendMultilineOp('+', txt);
} else if (o!.skip) {
const txt = textLeft.substring(0, o!.skip);
textLeft = textLeft.substring(o!.skip);
outTextAssem.append(txt);
appendMultilineOp('=', txt);
} else if (o!.remove) {
const txt = textLeft.substring(0, o!.remove);
textLeft = textLeft.substring(o!.remove);
appendMultilineOp('-', txt);
}
};
while (textLeft.length > 1) doOp();
for (let i = 0; i < 5; i++) doOp(); // do some more (only insertions will happen)
const outText = `${outTextAssem.toString()}\n`;
opAssem.endDocument();
const cs = pack(oldLen, outText.length, opAssem.toString(), charBank.toString());
checkRep(cs);
return [cs, outText];
};

View file

@ -0,0 +1,47 @@
import {expect, describe, it} from 'vitest'
import {StringIterator} from "../../../static/js/StringIterator";
describe('Test string iterator take', function () {
it('should iterate over a string', async function () {
const str = 'Hello, world!';
const iter = new StringIterator(str);
let i = 0;
while (iter.remaining() > 0) {
expect(iter.remaining()).to.equal(str.length - i);
console.error(iter.remaining());
expect(iter.take(1)).to.equal(str.charAt(i));
i++;
}
});
})
describe('Test string iterator peek', function () {
it('should peek over a string', async function () {
const str = 'Hello, world!';
const iter = new StringIterator(str);
let i = 0;
while (iter.remaining() > 0) {
expect(iter.remaining()).to.equal(str.length - i);
expect(iter.peek(1)).to.equal(str.charAt(i));
i++;
iter.skip(1);
}
});
})
describe('Test string iterator skip', function () {
it('should throw error when skip over a string too long', async function () {
const str = 'Hello, world!';
const iter = new StringIterator(str);
expect(()=>iter.skip(1000)).toThrowError();
});
it('should skip over a string', async function () {
const str = 'Hello, world!';
const iter = new StringIterator(str);
iter.skip(7);
expect(iter.take(1)).to.equal('w');
});
})

View file

@ -0,0 +1,224 @@
'use strict';
import {deserializeOps, opsFromAText} from '../../../static/js/Changeset';
import padutils from '../../../static/js/pad_utils';
import {poolOrArray} from '../easysync-helper.js';
import {describe, it, expect} from 'vitest'
import {OpAssembler} from "../../../static/js/OpAssembler";
import {SmartOpAssembler} from "../../../static/js/SmartOpAssembler";
import Op from "../../../static/js/Op";
describe('easysync-assembler', function () {
it('opAssembler', async function () {
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
const assem = new OpAssembler();
var opLength = 0
for (const op of deserializeOps(x)){
console.log(op)
assem.append(op);
opLength++
}
expect(assem.toString()).to.equal(x);
});
it('smartOpAssembler', async function () {
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal(x);
});
it('smartOpAssembler ignore additional pure keeps (no attributes)', async function () {
const x = '-c*3*4+6|1+1=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4+6|1+1');
});
it('smartOpAssembler merge consecutive + ops without multiline', async function () {
const x = '-c*3*4+6*3*4+1*3*4+9=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4+g');
});
it('smartOpAssembler merge consecutive + ops with multiline', async function () {
const x = '-c*3*4+6*3*4|1+1*3*4|9+f*3*4+k=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4|a+m*3*4+k');
});
it('smartOpAssembler merge consecutive - ops without multiline', async function () {
const x = '-c-6-1-9=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-s');
});
it('smartOpAssembler merge consecutive - ops with multiline', async function () {
const x = '-c-6|1-1|9-f-k=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('|a-y-k');
});
it('smartOpAssembler merge consecutive = ops without multiline', async function () {
const x = '-c*3*4=6*2*4=1*3*4=f*3*4=2*3*4=a=k=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4=6*2*4=1*3*4=r');
});
it('smartOpAssembler merge consecutive = ops with multiline', async function () {
const x = '-c*3*4=6*2*4|1=1*3*4|9=f*3*4|2=2*3*4=a*3*4=1=k=5';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4=6*2*4|1=1*3*4|b=h*3*4=b');
});
it('smartOpAssembler ignore + ops with ops.chars === 0', async function () {
const x = '-c*3*4+6*3*4+0*3*4+1+0*3*4+1';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-c*3*4+8');
});
it('smartOpAssembler ignore - ops with ops.chars === 0', async function () {
const x = '-c-6-0-1-0-1';
const assem = new SmartOpAssembler();
for (const op of deserializeOps(x)) assem.append(op);
assem.endDocument();
expect(assem.toString()).to.equal('-k');
});
it('smartOpAssembler append + op with text', async function () {
const assem = new SmartOpAssembler();
const pool = poolOrArray([
'attr1,1',
'attr2,2',
'attr3,3',
'attr4,4',
'attr5,5',
]);
padutils.warnDeprecatedFlags.disabledForTestingOnly = true;
try {
assem.appendOpWithText('+', 'test', '*3*4*5', pool);
assem.appendOpWithText('+', 'test', '*3*4*5', pool);
assem.appendOpWithText('+', 'test', '*1*4*5', pool);
} finally {
// @ts-ignore
delete padutils.warnDeprecatedFlags.disabledForTestingOnly;
}
assem.endDocument();
expect(assem.toString()).to.equal('*3*4*5+8*1*4*5+4');
});
it('smartOpAssembler append + op with multiline text', async function () {
const assem = new SmartOpAssembler();
const pool = poolOrArray([
'attr1,1',
'attr2,2',
'attr3,3',
'attr4,4',
'attr5,5',
]);
padutils.warnDeprecatedFlags.disabledForTestingOnly = true;
try {
assem.appendOpWithText('+', 'test\ntest', '*3*4*5', pool);
assem.appendOpWithText('+', '\ntest\n', '*3*4*5', pool);
assem.appendOpWithText('+', '\ntest', '*1*4*5', pool);
} finally {
// @ts-ignore
delete padutils.warnDeprecatedFlags.disabledForTestingOnly;
}
assem.endDocument();
expect(assem.toString()).to.equal('*3*4*5|3+f*1*4*5|1+1*1*4*5+4');
});
it('smartOpAssembler clear should empty internal assemblers', async function () {
const x = '-c*3*4+6|3=az*asdf0*1*2*3+1=1-1+1*0+1=1-1+1|c=c-1';
const ops = deserializeOps(x);
const iter = {
_n: ops.next(),
hasNext() { return !this._n.done; },
next() { const v = this._n.value; this._n = ops.next(); return v as Op; },
};
const assem = new SmartOpAssembler();
var iter1 = iter.next()
assem.append(iter1);
var iter2 = iter.next()
assem.append(iter2);
var iter3 = iter.next()
assem.append(iter3);
console.log(assem.toString());
assem.clear();
assem.append(iter.next());
assem.append(iter.next());
console.log(assem.toString());
assem.clear();
let counter = 0;
while (iter.hasNext()) {
console.log(counter++)
assem.append(iter.next());
}
assem.endDocument();
expect(assem.toString()).to.equal('-1+1*0+1=1-1+1|c=c-1');
});
describe('append atext to assembler', function () {
const testAppendATextToAssembler = (testId: number, atext: { text: string; attribs: string; }, correctOps: string) => {
it(`testAppendATextToAssembler#${testId}`, async function () {
const assem = new SmartOpAssembler();
for (const op of opsFromAText(atext)) assem.append(op);
expect(assem.toString()).to.equal(correctOps);
});
};
testAppendATextToAssembler(1, {
text: '\n',
attribs: '|1+1',
}, '');
testAppendATextToAssembler(2, {
text: '\n\n',
attribs: '|2+2',
}, '|1+1');
testAppendATextToAssembler(3, {
text: '\n\n',
attribs: '*x|2+2',
}, '*x|1+1');
testAppendATextToAssembler(4, {
text: '\n\n',
attribs: '*x|1+1|1+1',
}, '*x|1+1');
testAppendATextToAssembler(5, {
text: 'foo\n',
attribs: '|1+4',
}, '+3');
testAppendATextToAssembler(6, {
text: '\nfoo\n',
attribs: '|2+5',
}, '|1+1+3');
testAppendATextToAssembler(7, {
text: '\nfoo\n',
attribs: '*x|2+5',
}, '*x|1+1*x+3');
testAppendATextToAssembler(8, {
text: '\n\n\nfoo\n',
attribs: '|2+2*x|2+5',
}, '|2+2*x|1+1*x+3');
});
});

View file

@ -0,0 +1,54 @@
'use strict';
import {applyToText, checkRep, compose} from '../../../static/js/Changeset';
import AttributePool from '../../../static/js/AttributePool';
import {randomMultiline, randomTestChangeset} from '../easysync-helper';
import {expect, describe, it} from 'vitest';
describe('easysync-compose', function () {
describe('compose', function () {
const testCompose = (randomSeed: number) => {
it(`testCompose#${randomSeed}`, async function () {
const p = new AttributePool();
const startText = `${randomMultiline(10, 20)}\n`;
const x1 = randomTestChangeset(startText);
const change1 = x1[0];
const text1 = x1[1];
const x2 = randomTestChangeset(text1);
const change2 = x2[0];
const text2 = x2[1];
const x3 = randomTestChangeset(text2);
const change3 = x3[0];
const text3 = x3[1];
const change12 = checkRep(compose(change1, change2, p));
const change23 = checkRep(compose(change2, change3, p));
const change123 = checkRep(compose(change12, change3, p));
const change123a = checkRep(compose(change1, change23, p));
expect(change123a).to.equal(change123);
expect(applyToText(change12, startText)).to.equal(text2);
expect(applyToText(change23, text1)).to.equal(text3);
expect(applyToText(change123, startText)).to.equal(text3);
});
};
for (let i = 0; i < 30; i++) testCompose(i);
});
describe('compose attributes', function () {
it('simpleComposeAttributesTest', async function () {
const p = new AttributePool();
p.putAttrib(['bold', '']);
p.putAttrib(['bold', 'true']);
const cs1 = checkRep('Z:2>1*1+1*1=1$x');
const cs2 = checkRep('Z:3>0*0|1=3$');
const cs12 = checkRep(compose(cs1, cs2, p));
expect(cs12).to.equal('Z:2>1+1*0|1=2$x');
});
});
});

View file

@ -0,0 +1,320 @@
'use strict';
import {applyToAttribution, applyToText, checkRep, joinAttributionLines, mutateAttributionLines, mutateTextLines, pack} from '../../../static/js/Changeset';
import AttributePool from '../../../static/js/AttributePool';
import {poolOrArray} from '../easysync-helper';
import {expect, describe,it } from "vitest";
import {SmartOpAssembler} from "../../../static/js/SmartOpAssembler";
import Op from "../../../static/js/Op";
import {StringAssembler} from "../../../static/js/StringAssembler";
import TextLinesMutator from "../../../static/js/TextLinesMutator";
import {numToString} from "../../../static/js/ChangesetUtils";
describe('easysync-mutations', function () {
const applyMutations = (mu: TextLinesMutator, arrayOfArrays: any[]) => {
arrayOfArrays.forEach((a) => {
// @ts-ignore
const result = mu[a[0]](...a.slice(1));
if (a[0] === 'remove' && a[3]) {
expect(result).to.equal(a[3]);
}
});
};
const mutationsToChangeset = (oldLen: number, arrayOfArrays: string[][]) => {
const assem = new SmartOpAssembler();
const op = new Op();
const bank = new StringAssembler();
let oldPos = 0;
let newLen = 0;
arrayOfArrays.forEach((a: any[]) => {
if (a[0] === 'skip') {
op.opcode = '=';
op.chars = a[1];
op.lines = (a[2] || 0);
assem.append(op);
oldPos += op.chars;
newLen += op.chars;
} else if (a[0] === 'remove') {
op.opcode = '-';
op.chars = a[1];
op.lines = (a[2] || 0);
assem.append(op);
oldPos += op.chars;
} else if (a[0] === 'insert') {
op.opcode = '+';
bank.append(a[1]);
op.chars = a[1].length;
op.lines = (a[2] || 0);
assem.append(op);
newLen += op.chars;
}
});
newLen += oldLen - oldPos;
assem.endDocument();
return pack(oldLen, newLen, assem.toString(), bank.toString());
};
const runMutationTest = (testId: number, origLines: string[], muts:any, correct: string[]) => {
it(`runMutationTest#${testId}`, async function () {
let lines = origLines.slice();
const mu = new TextLinesMutator(lines);
applyMutations(mu, muts);
mu.close();
expect(lines).to.eql(correct);
const inText = origLines.join('');
const cs = mutationsToChangeset(inText.length, muts);
lines = origLines.slice();
mutateTextLines(cs, lines);
expect(lines).to.eql(correct);
const correctText = correct.join('');
const outText = applyToText(cs, inText);
expect(outText).to.equal(correctText);
});
};
runMutationTest(1, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [
['remove', 1, 0, 'a'],
['insert', 'tu'],
['remove', 1, 0, 'p'],
['skip', 4, 1],
['skip', 7, 1],
['insert', 'cream\npie\n', 2],
['skip', 2],
['insert', 'bot'],
['insert', '\n', 1],
['insert', 'bu'],
['skip', 3],
['remove', 3, 1, 'ge\n'],
['remove', 6, 0, 'duffle'],
], ['tuple\n', 'banana\n', 'cream\n', 'pie\n', 'cabot\n', 'bubba\n', 'eggplant\n']);
runMutationTest(2, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [
['remove', 1, 0, 'a'],
['remove', 1, 0, 'p'],
['insert', 'tu'],
['skip', 11, 2],
['insert', 'cream\npie\n', 2],
['skip', 2],
['insert', 'bot'],
['insert', '\n', 1],
['insert', 'bu'],
['skip', 3],
['remove', 3, 1, 'ge\n'],
['remove', 6, 0, 'duffle'],
], ['tuple\n', 'banana\n', 'cream\n', 'pie\n', 'cabot\n', 'bubba\n', 'eggplant\n']);
runMutationTest(3, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [
['remove', 6, 1, 'apple\n'],
['skip', 15, 2],
['skip', 6],
['remove', 1, 1, '\n'],
['remove', 8, 0, 'eggplant'],
['skip', 1, 1],
], ['banana\n', 'cabbage\n', 'duffle\n']);
runMutationTest(4, ['15\n'], [
['skip', 1],
['insert', '\n2\n3\n4\n', 4],
['skip', 2, 1],
], ['1\n', '2\n', '3\n', '4\n', '5\n']);
runMutationTest(5, ['1\n', '2\n', '3\n', '4\n', '5\n'], [
['skip', 1],
['remove', 7, 4, '\n2\n3\n4\n'],
['skip', 2, 1],
], ['15\n']);
runMutationTest(6, ['123\n', 'abc\n', 'def\n', 'ghi\n', 'xyz\n'], [
['insert', '0'],
['skip', 4, 1],
['skip', 4, 1],
['remove', 8, 2, 'def\nghi\n'],
['skip', 4, 1],
], ['0123\n', 'abc\n', 'xyz\n']);
runMutationTest(7, ['apple\n', 'banana\n', 'cabbage\n', 'duffle\n', 'eggplant\n'], [
['remove', 6, 1, 'apple\n'],
['skip', 15, 2, true],
['skip', 6, 0, true],
['remove', 1, 1, '\n'],
['remove', 8, 0, 'eggplant'],
['skip', 1, 1, true],
], ['banana\n', 'cabbage\n', 'duffle\n']);
it('mutatorHasMore', async function () {
const lines = ['1\n', '2\n', '3\n', '4\n'];
let mu;
mu = new TextLinesMutator(lines);
expect(mu.hasMore()).toBeTruthy();
mu.skip(8, 4);
expect(mu.hasMore()).toBeFalsy();
mu.close();
expect(mu.hasMore()).toBeFalsy();
// still 1,2,3,4
mu = new TextLinesMutator(lines);
expect(mu.hasMore()).toBeTruthy();
mu.remove(2, 1);
expect(mu.hasMore()).toBeTruthy();
mu.skip(2, 1);
expect(mu.hasMore()).toBeTruthy();
mu.skip(2, 1);
expect(mu.hasMore()).toBeTruthy();
mu.skip(2, 1);
expect(mu.hasMore()).toBeFalsy();
mu.insert('5\n', 1);
expect(mu.hasMore()).toBeFalsy();
mu.close();
expect(mu.hasMore()).toBeFalsy();
// 2,3,4,5 now
mu = new TextLinesMutator(lines);
expect(mu.hasMore()).toBeTruthy();
mu.remove(6, 3);
expect(mu.hasMore()).toBeTruthy();
mu.remove(2, 1);
expect(mu.hasMore()).toBeFalsy();
mu.insert('hello\n', 1);
expect(mu.hasMore()).toBeFalsy();
mu.close();
expect(mu.hasMore()).toBeFalsy();
});
describe('mutateTextLines', function () {
const testMutateTextLines = (testId: number, cs: string, lines: string[], correctLines: string[]) => {
it(`testMutateTextLines#${testId}`, async function () {
const a = lines.slice();
mutateTextLines(cs, a);
expect(a).to.eql(correctLines);
});
};
testMutateTextLines(1, 'Z:4<1|1-2-1|1+1+1$\nc', ['a\n', 'b\n'], ['\n', 'c\n']);
testMutateTextLines(2, 'Z:4>0|1-2-1|2+3$\nc\n', ['a\n', 'b\n'], ['\n', 'c\n', '\n']);
it('mutate keep only lines', async function () {
const lines = ['1\n', '2\n', '3\n', '4\n'];
const result = lines.slice();
const cs = 'Z:8>0*0|1=2|2=2';
mutateTextLines(cs, lines);
expect(result).to.eql(lines);
});
});
describe('mutate attributions', function () {
const testPoolWithChars = (() => {
const p = new AttributePool();
p.putAttrib(['char', 'newline']);
for (let i = 1; i < 36; i++) {
p.putAttrib(['char', numToString(i)]);
}
p.putAttrib(['char', '']);
return p;
})();
const runMutateAttributionTest = (testId: number, attribs: string[] | AttributePool, cs: string, alines: string[], outCorrect: string[]) => {
it(`runMutateAttributionTest#${testId}`, async function () {
const p = poolOrArray(attribs);
const alines2 = Array.prototype.slice.call(alines);
mutateAttributionLines(checkRep(cs), alines2, p);
expect(alines2).to.eql(outCorrect);
const removeQuestionMarks = (a: string) => a.replace(/\?/g, '');
const inMerged = joinAttributionLines(alines.map(removeQuestionMarks));
const correctMerged = joinAttributionLines(outCorrect.map(removeQuestionMarks));
const mergedResult = applyToAttribution(cs, inMerged, p);
expect(mergedResult).to.equal(correctMerged);
});
};
// turn 123\n 456\n 789\n into 123\n 4<b>5</b>6\n 789\n
runMutateAttributionTest(1,
['bold,true'], 'Z:c>0|1=4=1*0=1$', ['|1+4', '|1+4', '|1+4'],
['|1+4', '+1*0+1|1+2', '|1+4']);
// make a document bold
runMutateAttributionTest(2,
['bold,true'], 'Z:c>0*0|3=c$', ['|1+4', '|1+4', '|1+4'], ['*0|1+4', '*0|1+4', '*0|1+4']);
// clear bold on document
runMutateAttributionTest(3,
['bold,', 'bold,true'], 'Z:c>0*0|3=c$',
['*1+1+1*1+1|1+1', '+1*1+1|1+2', '*1+1+1*1+1|1+1'], ['|1+4', '|1+4', '|1+4']);
// add a character on line 3 of a document with 5 blank lines, and make sure
// the optimization that skips purely-kept lines is working; if any attribution string
// with a '?' is parsed it will cause an error.
runMutateAttributionTest(4,
['foo,bar', 'line,1', 'line,2', 'line,3', 'line,4', 'line,5'],
'Z:5>1|2=2+1$x', ['?*1|1+1', '?*2|1+1', '*3|1+1', '?*4|1+1', '?*5|1+1'],
['?*1|1+1', '?*2|1+1', '+1*3|1+1', '?*4|1+1', '?*5|1+1']);
// based on runMutationTest#1
runMutateAttributionTest(5, testPoolWithChars,
'Z:11>7-2*t+1*u+1|2=b|2+a=2*b+1*o+1*t+1*0|1+1*b+1*u+1=3|1-3-6$tucream\npie\nbot\nbu',
[
'*a+1*p+2*l+1*e+1*0|1+1',
'*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1',
'*c+1*a+1*b+2*a+1*g+1*e+1*0|1+1',
'*d+1*u+1*f+2*l+1*e+1*0|1+1',
'*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1',
],
[
'*t+1*u+1*p+1*l+1*e+1*0|1+1',
'*b+1*a+1*n+1*a+1*n+1*a+1*0|1+1',
'|1+6',
'|1+4',
'*c+1*a+1*b+1*o+1*t+1*0|1+1',
'*b+1*u+1*b+2*a+1*0|1+1',
'*e+1*g+2*p+1*l+1*a+1*n+1*t+1*0|1+1',
]);
// based on runMutationTest#3
runMutateAttributionTest(6, testPoolWithChars,
'Z:11<f|1-6|2=f=6|1-1-8$', ['*a|1+6', '*b|1+7', '*c|1+8', '*d|1+7', '*e|1+9'],
['*b|1+7', '*c|1+8', '*d+6*e|1+1']);
// based on runMutationTest#4
runMutateAttributionTest(7, testPoolWithChars, 'Z:3>7=1|4+7$\n2\n3\n4\n',
['*1+1*5|1+2'], ['*1+1|1+1', '|1+2', '|1+2', '|1+2', '*5|1+2']);
// based on runMutationTest#5
runMutateAttributionTest(8, testPoolWithChars, 'Z:a<7=1|4-7$',
['*1|1+2', '*2|1+2', '*3|1+2', '*4|1+2', '*5|1+2'], ['*1+1*5|1+2']);
// based on runMutationTest#6
runMutateAttributionTest(9, testPoolWithChars, 'Z:k<7*0+1*10|2=8|2-8$0',
[
'*1+1*2+1*3+1|1+1',
'*a+1*b+1*c+1|1+1',
'*d+1*e+1*f+1|1+1',
'*g+1*h+1*i+1|1+1',
'?*x+1*y+1*z+1|1+1',
],
['*0+1|1+4', '|1+4', '?*x+1*y+1*z+1|1+1']);
runMutateAttributionTest(10, testPoolWithChars, 'Z:6>4=1+1=1+1|1=1+1=1*0+1$abcd',
['|1+3', '|1+3'], ['|1+5', '+2*0+1|1+2']);
runMutateAttributionTest(11, testPoolWithChars, 'Z:s>1|1=4=6|1+1$\n',
['*0|1+4', '*0|1+8', '*0+5|1+1', '*0|1+1', '*0|1+5', '*0|1+1', '*0|1+1', '*0|1+1', '|1+1'],
[
'*0|1+4',
'*0+6|1+1',
'*0|1+2',
'*0+5|1+1',
'*0|1+1',
'*0|1+5',
'*0|1+1',
'*0|1+1',
'*0|1+1',
'|1+1',
]);
});
});

View file

@ -0,0 +1,166 @@
'use strict';
import {applyToAttribution, applyToText, checkRep, deserializeOps, exportedForTestingOnly, filterAttribNumbers, joinAttributionLines, makeAttribsString, makeSplice, moveOpsToNewPool, opAttributeValue, splitAttributionLines} from '../../../static/js/Changeset';
import AttributePool from '../../../static/js/AttributePool';
import {randomMultiline, poolOrArray} from '../easysync-helper';
import padutils from '../../../static/js/pad_utils';
import {describe, it, expect} from 'vitest'
import Op from "../../../static/js/Op";
import {MergingOpAssembler} from "../../../static/js/MergingOpAssembler";
import {Attribute} from "../../../static/js/types/Attribute";
describe('easysync-other', function () {
describe('filter attribute numbers', function () {
const testFilterAttribNumbers = (testId: number, cs: string, filter: Function, correctOutput: string) => {
it(`testFilterAttribNumbers#${testId}`, async function () {
const str = filterAttribNumbers(cs, filter);
expect(str).to.equal(correctOutput);
});
};
testFilterAttribNumbers(1, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6',
(n: number) => (n % 2) === 0, '*0+1+2+3+4*2+5*0*2*c+6');
testFilterAttribNumbers(2, '*0*1+1+2+3*1+4*2+5*0*2*1*b*c+6',
(n: number) => (n % 2) === 1, '*1+1+2+3*1+4+5*1*b+6');
});
describe('make attribs string', function () {
const testMakeAttribsString = (testId: number, pool: string[], opcode: string, attribs: string | Attribute[], correctString: string) => {
it(`testMakeAttribsString#${testId}`, async function () {
const p = poolOrArray(pool);
padutils.warnDeprecatedFlags.disabledForTestingOnly = true;
try {
expect(makeAttribsString(opcode, attribs, p)).to.equal(correctString);
} finally {
// @ts-ignore
delete padutils.warnDeprecatedFlags.disabledForTestingOnly;
}
});
};
testMakeAttribsString(1, ['bold,'], '+', [
['bold', ''],
], '');
testMakeAttribsString(2, ['abc,def', 'bold,'], '=', [
['bold', ''],
], '*1');
testMakeAttribsString(3, ['abc,def', 'bold,true'], '+', [
['abc', 'def'],
['bold', 'true'],
], '*0*1');
testMakeAttribsString(4, ['abc,def', 'bold,true'], '+', [
['bold', 'true'],
['abc', 'def'],
], '*0*1');
});
describe('other', function () {
it('testMoveOpsToNewPool', async function () {
const pool1 = new AttributePool();
const pool2 = new AttributePool();
pool1.putAttrib(['baz', 'qux']);
pool1.putAttrib(['foo', 'bar']);
pool2.putAttrib(['foo', 'bar']);
expect(moveOpsToNewPool('Z:1>2*1+1*0+1$ab', pool1, pool2))
.to.equal('Z:1>2*0+1*1+1$ab');
expect(moveOpsToNewPool('*1+1*0+1', pool1, pool2)).to.equal('*0+1*1+1');
});
it('testMakeSplice', async function () {
const t = 'a\nb\nc\n';
let splice = makeSplice(t, 5, 0, 'def')
const t2 = applyToText(splice, t);
expect(t2).to.equal('a\nb\ncdef\n');
});
it('makeSplice at the end', async function () {
const orig = '123';
const ins = '456';
expect(applyToText(makeSplice(orig, orig.length, 0, ins), orig))
.to.equal(`${orig}${ins}`);
});
it('testToSplices', async function () {
const cs = checkRep('Z:z>9*0=1=4-3+9=1|1-4-4+1*0+a$123456789abcdefghijk');
const correctSplices = [
[5, 8, '123456789'],
[9, 17, 'abcdefghijk'],
];
expect(exportedForTestingOnly.toSplices(cs)).to.eql(correctSplices);
});
it('opAttributeValue', async function () {
const p = new AttributePool();
p.putAttrib(['name', 'david']);
p.putAttrib(['color', 'green']);
const stringOp = (str: string) => deserializeOps(str).next().value as Op;
padutils.warnDeprecatedFlags.disabledForTestingOnly = true;
try {
expect(opAttributeValue(stringOp('*0*1+1'), 'name', p)).to.equal('david');
expect(opAttributeValue(stringOp('*0+1'), 'name', p)).to.equal('david');
expect(opAttributeValue(stringOp('*1+1'), 'name', p)).to.equal('');
expect(opAttributeValue(stringOp('+1'), 'name', p)).to.equal('');
expect(opAttributeValue(stringOp('*0*1+1'), 'color', p)).to.equal('green');
expect(opAttributeValue(stringOp('*1+1'), 'color', p)).to.equal('green');
expect(opAttributeValue(stringOp('*0+1'), 'color', p)).to.equal('');
expect(opAttributeValue(stringOp('+1'), 'color', p)).to.equal('');
} finally {
// @ts-ignore
delete padutils.warnDeprecatedFlags.disabledForTestingOnly;
}
});
describe('applyToAttribution', function () {
const runApplyToAttributionTest = (testId: number, attribs: string[], cs: string, inAttr: string, outCorrect: string) => {
it(`applyToAttribution#${testId}`, async function () {
const p = poolOrArray(attribs);
const result = applyToAttribution(checkRep(cs), inAttr, p);
expect(result).to.equal(outCorrect);
});
};
// turn c<b>a</b>ctus\n into a<b>c</b>tusabcd\n
runApplyToAttributionTest(1,
['bold,', 'bold,true'], 'Z:7>3-1*0=1*1=1=3+4$abcd', '+1*1+1|1+5', '+1*1+1|1+8');
// turn "david\ngreenspan\n" into "<b>david\ngreen</b>\n"
runApplyToAttributionTest(2,
['bold,', 'bold,true'], 'Z:g<4*1|1=6*1=5-4$', '|2+g', '*1|1+6*1+5|1+1');
});
describe('split/join attribution lines', function () {
const testSplitJoinAttributionLines = (randomSeed: number) => {
const stringToOps = (str: string) => {
const assem = new MergingOpAssembler();
const o = new Op('+');
o.chars = 1;
for (let i = 0; i < str.length; i++) {
const c = str.charAt(i);
o.lines = (c === '\n' ? 1 : 0);
o.attribs = (c === 'a' || c === 'b' ? `*${c}` : '');
assem.append(o);
}
return assem.toString();
};
it(`testSplitJoinAttributionLines#${randomSeed}`, async function () {
const doc = `${randomMultiline(10, 20)}\n`;
const theJoined = stringToOps(doc);
const theSplit = doc.match(/[^\n]*\n/g)!.map(stringToOps);
expect(splitAttributionLines(theJoined, doc)).to.eql(theSplit);
expect(joinAttributionLines(theSplit)).to.equal(theJoined);
});
};
for (let i = 0; i < 10; i++) testSplitJoinAttributionLines(i);
});
});
});

View file

@ -0,0 +1,55 @@
'use strict';
import {subattribution} from '../../../static/js/Changeset';
import {expect, describe, it} from 'vitest';
describe('easysync-subAttribution', function () {
const testSubattribution = (testId: number, astr: string, start: number, end: number | undefined, correctOutput: string) => {
it(`subattribution#${testId}`, async function () {
const str = subattribution(astr, start, end);
expect(str).to.equal(correctOutput);
});
};
testSubattribution(1, '+1', 0, 0, '');
testSubattribution(2, '+1', 0, 1, '+1');
testSubattribution(3, '+1', 0, undefined, '+1');
testSubattribution(4, '|1+1', 0, 0, '');
testSubattribution(5, '|1+1', 0, 1, '|1+1');
testSubattribution(6, '|1+1', 0, undefined, '|1+1');
testSubattribution(7, '*0+1', 0, 0, '');
testSubattribution(8, '*0+1', 0, 1, '*0+1');
testSubattribution(9, '*0+1', 0, undefined, '*0+1');
testSubattribution(10, '*0|1+1', 0, 0, '');
testSubattribution(11, '*0|1+1', 0, 1, '*0|1+1');
testSubattribution(12, '*0|1+1', 0, undefined, '*0|1+1');
testSubattribution(13, '*0+2+1*1+3', 0, 1, '*0+1');
testSubattribution(14, '*0+2+1*1+3', 0, 2, '*0+2');
testSubattribution(15, '*0+2+1*1+3', 0, 3, '*0+2+1');
testSubattribution(16, '*0+2+1*1+3', 0, 4, '*0+2+1*1+1');
testSubattribution(17, '*0+2+1*1+3', 0, 5, '*0+2+1*1+2');
testSubattribution(18, '*0+2+1*1+3', 0, 6, '*0+2+1*1+3');
testSubattribution(19, '*0+2+1*1+3', 0, 7, '*0+2+1*1+3');
testSubattribution(20, '*0+2+1*1+3', 0, undefined, '*0+2+1*1+3');
testSubattribution(21, '*0+2+1*1+3', 1, undefined, '*0+1+1*1+3');
testSubattribution(22, '*0+2+1*1+3', 2, undefined, '+1*1+3');
testSubattribution(23, '*0+2+1*1+3', 3, undefined, '*1+3');
testSubattribution(24, '*0+2+1*1+3', 4, undefined, '*1+2');
testSubattribution(25, '*0+2+1*1+3', 5, undefined, '*1+1');
testSubattribution(26, '*0+2+1*1+3', 6, undefined, '');
testSubattribution(27, '*0+2+1*1|1+3', 0, 1, '*0+1');
testSubattribution(28, '*0+2+1*1|1+3', 0, 2, '*0+2');
testSubattribution(29, '*0+2+1*1|1+3', 0, 3, '*0+2+1');
testSubattribution(30, '*0+2+1*1|1+3', 0, 4, '*0+2+1*1+1');
testSubattribution(31, '*0+2+1*1|1+3', 0, 5, '*0+2+1*1+2');
testSubattribution(32, '*0+2+1*1|1+3', 0, 6, '*0+2+1*1|1+3');
testSubattribution(33, '*0+2+1*1|1+3', 0, 7, '*0+2+1*1|1+3');
testSubattribution(34, '*0+2+1*1|1+3', 0, undefined, '*0+2+1*1|1+3');
testSubattribution(35, '*0+2+1*1|1+3', 1, undefined, '*0+1+1*1|1+3');
testSubattribution(36, '*0+2+1*1|1+3', 2, undefined, '+1*1|1+3');
testSubattribution(37, '*0+2+1*1|1+3', 3, undefined, '*1|1+3');
testSubattribution(38, '*0+2+1*1|1+3', 4, undefined, '*1|1+2');
testSubattribution(39, '*0+2+1*1|1+3', 5, undefined, '*1|1+1');
testSubattribution(40, '*0+2+1*1|1+3', 1, 5, '*0+1+1*1+2');
testSubattribution(41, '*0+2+1*1|1+3', 2, 6, '+1*1|1+3');
testSubattribution(42, '*0+2+1*1+3', 2, 6, '+1*1+3');
});

View file

@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ["tests/backend-new/**/*.ts"],
include: ["tests/backend-new/specs/**/*.ts"],
},
})