mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-01-19 14:13:34 +01:00
Feat/changeset ts (#6594)
* Migrated changeset * Added more tests. * Fixed test scopes
This commit is contained in:
parent
3dae23a1e5
commit
28e04bdf71
37 changed files with 2540 additions and 1310 deletions
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
||||
};
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
import AttributeMap from './AttributeMap';
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const attributes = require('./attributes');
|
||||
const underscore = require("underscore")
|
||||
import {compose, deserializeOps, isIdentity} from './Changeset';
|
||||
import {Builder} from "./Builder";
|
||||
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils';
|
||||
import attributes from './attributes';
|
||||
import underscore from "underscore";
|
||||
|
||||
const lineMarkerAttribute = 'lmkr';
|
||||
|
||||
|
@ -52,7 +51,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
if (!this.applyChangesetCallback) return changeset;
|
||||
|
||||
const cs = changeset.toString();
|
||||
if (!Changeset.isIdentity(cs)) {
|
||||
if (!isIdentity(cs)) {
|
||||
this.applyChangesetCallback(cs);
|
||||
}
|
||||
|
||||
|
@ -86,7 +85,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// as the range might not be continuous
|
||||
// due to the presence of line markers on the rows
|
||||
if (allChangesets) {
|
||||
allChangesets = Changeset.compose(
|
||||
allChangesets = compose(
|
||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||
} else {
|
||||
allChangesets = rowChangeset;
|
||||
|
@ -126,9 +125,9 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
* @param attribs an array of attributes
|
||||
*/
|
||||
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||
ChangesetUtils.buildKeepRange(
|
||||
const builder = new Builder(this.rep.lines.totalWidth());
|
||||
buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||
buildKeepRange(
|
||||
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
||||
return builder;
|
||||
},
|
||||
|
@ -151,7 +150,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// get `attributeName` attribute of first char of line
|
||||
const aline = this.rep.alines[lineNum];
|
||||
if (!aline) return '';
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
const [op] = deserializeOps(aline);
|
||||
if (op == null) return '';
|
||||
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
|
||||
},
|
||||
|
@ -164,7 +163,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// get attributes of first char of line
|
||||
const aline = this.rep.alines[lineNum];
|
||||
if (!aline) return [];
|
||||
const [op] = Changeset.deserializeOps(aline);
|
||||
const [op] = deserializeOps(aline);
|
||||
if (op == null) return [];
|
||||
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
||||
},
|
||||
|
@ -222,7 +221,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
let hasAttrib = true;
|
||||
|
||||
let indexIntoLine = 0;
|
||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
||||
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||
const opStartInLine = indexIntoLine;
|
||||
const opEndInLine = opStartInLine + op.chars;
|
||||
if (!hasIt(op.attribs)) {
|
||||
|
@ -259,7 +258,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
// we need to sum up how much characters each operations take until the wanted position
|
||||
let currentPointer = 0;
|
||||
|
||||
for (const currentOperation of Changeset.deserializeOps(aline)) {
|
||||
for (const currentOperation of deserializeOps(aline)) {
|
||||
currentPointer += currentOperation.chars;
|
||||
if (currentPointer <= column) continue;
|
||||
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
||||
|
@ -286,13 +285,13 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
*/
|
||||
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
let loc = [0, 0];
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
const builder = new Builder(this.rep.lines.totalWidth());
|
||||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||
|
||||
if (hasMarker) {
|
||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||
[attributeName, attributeValue],
|
||||
], this.rep.apool);
|
||||
} else {
|
||||
|
@ -315,7 +314,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
* @param attributeValue if given only attributes with equal value will be removed
|
||||
*/
|
||||
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
||||
const builder = new Builder(this.rep.lines.totalWidth());
|
||||
const hasMarker = this.lineHasMarker(lineNum);
|
||||
let found = false;
|
||||
|
||||
|
@ -334,16 +333,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
|||
return;
|
||||
}
|
||||
|
||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||
|
||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
||||
|
||||
// if we have marker and any of attributes don't need to have marker. we need delete it
|
||||
if (hasMarker && !countAttribsWithMarker) {
|
||||
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||
buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||
} else {
|
||||
ChangesetUtils.buildKeepRange(
|
||||
buildKeepRange(
|
||||
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
||||
}
|
||||
|
||||
|
|
108
src/static/js/Builder.ts
Normal file
108
src/static/js/Builder.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* Incrementally builds a Changeset.
|
||||
*
|
||||
* @typedef {object} Builder
|
||||
* @property {Function} insert -
|
||||
* @property {Function} keep -
|
||||
* @property {Function} keepText -
|
||||
* @property {Function} remove -
|
||||
* @property {Function} toString -
|
||||
*/
|
||||
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||
import Op from "./Op";
|
||||
import {StringAssembler} from "./StringAssembler";
|
||||
import AttributeMap from "./AttributeMap";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
import {opsFromText, pack} from "./Changeset";
|
||||
|
||||
/**
|
||||
* @param {number} oldLen - Old length
|
||||
* @returns {Builder}
|
||||
*/
|
||||
export class Builder {
|
||||
private readonly oldLen: number;
|
||||
private assem: SmartOpAssembler;
|
||||
private readonly o: Op;
|
||||
private charBank: StringAssembler;
|
||||
|
||||
constructor(oldLen: number) {
|
||||
this.oldLen = oldLen
|
||||
this.assem = new SmartOpAssembler()
|
||||
this.o = new Op()
|
||||
this.charBank = new StringAssembler()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} N - Number of characters to keep.
|
||||
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
|
||||
* character must be a newline.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
keep = (N: number, L?: number, attribs?: string|Attribute[], pool?: AttributePool): Builder => {
|
||||
this.o.opcode = '=';
|
||||
this.o.attribs = typeof attribs === 'string'
|
||||
? attribs : new AttributeMap(pool).update(attribs || []).toString();
|
||||
this.o.chars = N;
|
||||
this.o.lines = (L || 0);
|
||||
this.assem.append(this.o);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} text - Text to keep.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
keepText= (text: string, attribs?: string|Attribute[], pool?: AttributePool): Builder=> {
|
||||
for (const op of opsFromText('=', text, attribs, pool)) this.assem.append(op);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} text - Text to insert.
|
||||
* @param {(string|Attribute[])} attribs - Either [[key1,value1],[key2,value2],...] or '*0*1...'
|
||||
* (no pool needed in latter case).
|
||||
* @param {?AttributePool.ts} pool - Attribute pool, only required if `attribs` is a list of
|
||||
* attribute key, value pairs.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
insert= (text: string, attribs: string | Attribute[] | undefined, pool?: AttributePool | null | undefined): Builder => {
|
||||
for (const op of opsFromText('+', text, attribs, pool)) this.assem.append(op);
|
||||
this.charBank.append(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {number} N - Number of characters to remove.
|
||||
* @param {number} L - Number of newlines among the `N` characters. If positive, the last
|
||||
* character must be a newline.
|
||||
* @returns {Builder} this
|
||||
*/
|
||||
remove= (N: number, L?: number): Builder => {
|
||||
this.o.opcode = '-';
|
||||
this.o.attribs = '';
|
||||
this.o.chars = N;
|
||||
this.o.lines = (L || 0);
|
||||
this.assem.append(this.o);
|
||||
return this;
|
||||
}
|
||||
|
||||
toString= () => {
|
||||
this.assem.endDocument();
|
||||
const newLen = this.oldLen + this.assem.getLengthChange();
|
||||
return pack(this.oldLen, newLen, this.assem.toString(), this.charBank.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,3 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
@ -6,6 +5,12 @@
|
|||
* based on a SkipList
|
||||
*/
|
||||
|
||||
import {RepModel} from "./types/RepModel";
|
||||
import {ChangeSetBuilder} from "./types/ChangeSetBuilder";
|
||||
import {Attribute} from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
import {Builder} from "./Builder";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -21,7 +26,7 @@
|
|||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
exports.buildRemoveRange = (rep, builder, start, end) => {
|
||||
export const buildRemoveRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number,number], end: [number, number]) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
|
@ -33,7 +38,7 @@ exports.buildRemoveRange = (rep, builder, start, end) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
|
||||
export const buildKeepRange = (rep: RepModel, builder: ChangeSetBuilder, start: [number, number], end:[number, number], attribs?: Attribute[], pool?: AttributePool) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
const endLineOffset = rep.lines.offsetOfIndex(end[0]);
|
||||
|
||||
|
@ -45,9 +50,25 @@ exports.buildKeepRange = (rep, builder, start, end, attribs, pool) => {
|
|||
}
|
||||
};
|
||||
|
||||
exports.buildKeepToStartOfRange = (rep, builder, start) => {
|
||||
export const buildKeepToStartOfRange = (rep: RepModel, builder: Builder, start: [number, number]) => {
|
||||
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||
|
||||
builder.keep(startLineOffset, start[0]);
|
||||
builder.keep(start[1]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a number from string base 36.
|
||||
*
|
||||
* @param {string} str - string of the number in base 36
|
||||
* @returns {number} number
|
||||
*/
|
||||
export const parseNum = (str: string): number => parseInt(str, 36);
|
||||
|
||||
/**
|
||||
* Writes a number in base 36 and puts it in a string.
|
||||
*
|
||||
* @param {number} num - number
|
||||
* @returns {string} string
|
||||
*/
|
||||
export const numToString = (num: number): string => num.toString(36).toLowerCase();
|
||||
|
|
73
src/static/js/MergingOpAssembler.ts
Normal file
73
src/static/js/MergingOpAssembler.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import {OpAssembler} from "./OpAssembler";
|
||||
import Op from "./Op";
|
||||
import {clearOp, copyOp} from "./Changeset";
|
||||
|
||||
export class MergingOpAssembler {
|
||||
private assem: OpAssembler;
|
||||
private readonly bufOp: Op;
|
||||
private bufOpAdditionalCharsAfterNewline: number;
|
||||
|
||||
constructor() {
|
||||
this.assem = new OpAssembler()
|
||||
this.bufOp = new Op()
|
||||
// If we get, for example, insertions [xxx\n,yyy], those don't merge,
|
||||
// but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
|
||||
// This variable stores the length of yyy and any other newline-less
|
||||
// ops immediately after it.
|
||||
this.bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} [isEndDocument]
|
||||
*/
|
||||
flush = (isEndDocument?: boolean) => {
|
||||
if (!this.bufOp.opcode) return;
|
||||
if (isEndDocument && this.bufOp.opcode === '=' && !this.bufOp.attribs) {
|
||||
// final merged keep, leave it implicit
|
||||
} else {
|
||||
this.assem.append(this.bufOp);
|
||||
if (this.bufOpAdditionalCharsAfterNewline) {
|
||||
this.bufOp.chars = this.bufOpAdditionalCharsAfterNewline;
|
||||
this.bufOp.lines = 0;
|
||||
this.assem.append(this.bufOp);
|
||||
this.bufOpAdditionalCharsAfterNewline = 0;
|
||||
}
|
||||
}
|
||||
this.bufOp.opcode = '';
|
||||
}
|
||||
|
||||
append = (op: Op) => {
|
||||
if (op.chars <= 0) return;
|
||||
if (this.bufOp.opcode === op.opcode && this.bufOp.attribs === op.attribs) {
|
||||
if (op.lines > 0) {
|
||||
// bufOp and additional chars are all mergeable into a multi-line op
|
||||
this.bufOp.chars += this.bufOpAdditionalCharsAfterNewline + op.chars;
|
||||
this.bufOp.lines += op.lines;
|
||||
this.bufOpAdditionalCharsAfterNewline = 0;
|
||||
} else if (this.bufOp.lines === 0) {
|
||||
// both bufOp and op are in-line
|
||||
this.bufOp.chars += op.chars;
|
||||
} else {
|
||||
// append in-line text to multi-line bufOp
|
||||
this.bufOpAdditionalCharsAfterNewline += op.chars;
|
||||
}
|
||||
} else {
|
||||
this.flush();
|
||||
copyOp(op, this.bufOp);
|
||||
}
|
||||
}
|
||||
|
||||
endDocument = () => {
|
||||
this.flush(true);
|
||||
};
|
||||
|
||||
toString = () => {
|
||||
this.flush();
|
||||
return this.assem.toString();
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.assem.clear();
|
||||
clearOp(this.bufOp);
|
||||
};
|
||||
}
|
78
src/static/js/Op.ts
Normal file
78
src/static/js/Op.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import {numToString} from "./ChangesetUtils";
|
||||
|
||||
export type OpCode = ''|'='|'+'|'-';
|
||||
|
||||
|
||||
/**
|
||||
* An operation to apply to a shared document.
|
||||
*/
|
||||
export default class Op {
|
||||
opcode: ''|'='|'+'|'-'
|
||||
chars: number
|
||||
lines: number
|
||||
attribs: string
|
||||
/**
|
||||
* @param {(''|'='|'+'|'-')} [opcode=''] - Initial value of the `opcode` property.
|
||||
*/
|
||||
constructor(opcode:''|'='|'+'|'-' = '') {
|
||||
/**
|
||||
* The operation's operator:
|
||||
* - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
|
||||
* document.
|
||||
* - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
|
||||
* document.
|
||||
* - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
|
||||
* the document. The inserted characters come from the changeset's character bank.
|
||||
* - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
|
||||
* operation.
|
||||
*
|
||||
* @type {(''|'='|'+'|'-')}
|
||||
* @public
|
||||
*/
|
||||
this.opcode = opcode;
|
||||
|
||||
/**
|
||||
* The number of characters to keep, insert, or delete.
|
||||
*
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.chars = 0;
|
||||
|
||||
/**
|
||||
* The number of characters among the `chars` characters that are newlines. If non-zero, the
|
||||
* last character must be a newline.
|
||||
*
|
||||
* @type {number}
|
||||
* @public
|
||||
*/
|
||||
this.lines = 0;
|
||||
|
||||
/**
|
||||
* Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
|
||||
* sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
|
||||
* '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
|
||||
* identifiers come from the document's attribute pool.
|
||||
*
|
||||
* For keep ('=') operations, the attributes are merged with the base text's existing
|
||||
* attributes:
|
||||
* - A keep op attribute with a non-empty value replaces an existing base text attribute that
|
||||
* has the same key.
|
||||
* - A keep op attribute with an empty value is interpreted as an instruction to remove an
|
||||
* existing base text attribute that has the same key, if one exists.
|
||||
*
|
||||
* This is the empty string for remove ('-') operations.
|
||||
*
|
||||
* @type {string}
|
||||
* @public
|
||||
*/
|
||||
this.attribs = '';
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (!this.opcode) throw new TypeError('null op');
|
||||
if (typeof this.attribs !== 'string') throw new TypeError('attribs must be a string');
|
||||
const l = this.lines ? `|${numToString(this.lines)}` : '';
|
||||
return this.attribs + l + this.opcode + numToString(this.chars);
|
||||
}
|
||||
}
|
21
src/static/js/OpAssembler.ts
Normal file
21
src/static/js/OpAssembler.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Op from "./Op";
|
||||
import {assert} from './Changeset'
|
||||
|
||||
/**
|
||||
* @returns {OpAssembler}
|
||||
*/
|
||||
export class OpAssembler {
|
||||
private serialized: string;
|
||||
constructor() {
|
||||
this.serialized = ''
|
||||
|
||||
}
|
||||
append = (op: Op) => {
|
||||
assert(op instanceof Op, 'argument must be an instance of Op');
|
||||
this.serialized += op.toString();
|
||||
}
|
||||
toString = () => this.serialized
|
||||
clear = () => {
|
||||
this.serialized = '';
|
||||
}
|
||||
}
|
47
src/static/js/OpIter.ts
Normal file
47
src/static/js/OpIter.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import Op from "./Op";
|
||||
import {clearOp, copyOp, deserializeOps} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Iterator over a changeset's operations.
|
||||
*
|
||||
* Note: This class does NOT implement the ECMAScript iterable or iterator protocols.
|
||||
*
|
||||
* @deprecated Use `deserializeOps` instead.
|
||||
*/
|
||||
export class OpIter {
|
||||
private gen
|
||||
private _next: IteratorResult<Op, void>
|
||||
/**
|
||||
* @param {string} ops - String encoding the change operations to iterate over.
|
||||
*/
|
||||
constructor(ops: string) {
|
||||
this.gen = deserializeOps(ops);
|
||||
this._next = this.gen.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} Whether there are any remaining operations.
|
||||
*/
|
||||
hasNext(): boolean {
|
||||
return !this._next.done;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next operation object and advances the iterator.
|
||||
*
|
||||
* Note: This does NOT implement the ECMAScript iterator protocol.
|
||||
*
|
||||
* @param {Op} [opOut] - Deprecated. Operation object to recycle for the return value.
|
||||
* @returns {Op} The next operation, or an operation with a falsy `opcode` property if there are
|
||||
* no more operations.
|
||||
*/
|
||||
next(opOut: Op = new Op()): Op {
|
||||
if (this.hasNext()) {
|
||||
copyOp(this._next.value!, opOut);
|
||||
this._next = this.gen.next();
|
||||
} else {
|
||||
clearOp(opOut);
|
||||
}
|
||||
return opOut;
|
||||
}
|
||||
}
|
115
src/static/js/SmartOpAssembler.ts
Normal file
115
src/static/js/SmartOpAssembler.ts
Normal file
|
@ -0,0 +1,115 @@
|
|||
import {MergingOpAssembler} from "./MergingOpAssembler";
|
||||
import {StringAssembler} from "./StringAssembler";
|
||||
import padutils from "./pad_utils";
|
||||
import Op from "./Op";
|
||||
import { Attribute } from "./types/Attribute";
|
||||
import AttributePool from "./AttributePool";
|
||||
import {opsFromText} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Creates an object that allows you to append operations (type Op) and also compresses them if
|
||||
* possible. Like MergingOpAssembler, but able to produce conforming exportss from slightly looser
|
||||
* input, at the cost of speed. Specifically:
|
||||
* - merges consecutive operations that can be merged
|
||||
* - strips final "="
|
||||
* - ignores 0-length changes
|
||||
* - reorders consecutive + and - (which MergingOpAssembler doesn't do)
|
||||
*
|
||||
* @typedef {object} SmartOpAssembler
|
||||
* @property {Function} append -
|
||||
* @property {Function} appendOpWithText -
|
||||
* @property {Function} clear -
|
||||
* @property {Function} endDocument -
|
||||
* @property {Function} getLengthChange -
|
||||
* @property {Function} toString -
|
||||
*/
|
||||
export class SmartOpAssembler {
|
||||
private minusAssem: MergingOpAssembler;
|
||||
private plusAssem: MergingOpAssembler;
|
||||
private keepAssem: MergingOpAssembler;
|
||||
private lastOpcode: string;
|
||||
private lengthChange: number;
|
||||
private assem: StringAssembler;
|
||||
|
||||
constructor() {
|
||||
this.minusAssem = new MergingOpAssembler()
|
||||
this.plusAssem = new MergingOpAssembler()
|
||||
this.keepAssem = new MergingOpAssembler()
|
||||
this.assem = new StringAssembler()
|
||||
this.lastOpcode = ''
|
||||
this.lengthChange = 0
|
||||
}
|
||||
|
||||
flushKeeps = () => {
|
||||
this.assem.append(this.keepAssem.toString());
|
||||
this.keepAssem.clear();
|
||||
};
|
||||
|
||||
flushPlusMinus = () => {
|
||||
this.assem.append(this.minusAssem.toString());
|
||||
this.minusAssem.clear();
|
||||
this.assem.append(this.plusAssem.toString());
|
||||
this.plusAssem.clear();
|
||||
};
|
||||
|
||||
append = (op: Op) => {
|
||||
if (!op.opcode) return;
|
||||
if (!op.chars) return;
|
||||
|
||||
if (op.opcode === '-') {
|
||||
if (this.lastOpcode === '=') {
|
||||
this.flushKeeps();
|
||||
}
|
||||
this.minusAssem.append(op);
|
||||
this.lengthChange -= op.chars;
|
||||
} else if (op.opcode === '+') {
|
||||
if (this.lastOpcode === '=') {
|
||||
this.flushKeeps();
|
||||
}
|
||||
this.plusAssem.append(op);
|
||||
this.lengthChange += op.chars;
|
||||
} else if (op.opcode === '=') {
|
||||
if (this.lastOpcode !== '=') {
|
||||
this.flushPlusMinus();
|
||||
}
|
||||
this.keepAssem.append(op);
|
||||
}
|
||||
this.lastOpcode = op.opcode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates operations from the given text and attributes.
|
||||
*
|
||||
* @deprecated Use `opsFromText` instead.
|
||||
* @param {('-'|'+'|'=')} opcode - The operator to use.
|
||||
* @param {string} text - The text to remove/add/keep.
|
||||
* @param {(string|Iterable<Attribute>)} attribs - The attributes to apply to the operations.
|
||||
* @param {?AttributePool.ts} pool - Attribute pool. Only required if `attribs` is an iterable of
|
||||
* attribute key, value pairs.
|
||||
*/
|
||||
appendOpWithText = (opcode: '-'|'+'|'=', text: string, attribs: Attribute[]|string, pool?: AttributePool) => {
|
||||
padutils.warnDeprecated('Changeset.smartOpAssembler().appendOpWithText() is deprecated; ' +
|
||||
'use opsFromText() instead.');
|
||||
for (const op of opsFromText(opcode, text, attribs, pool)) this.append(op);
|
||||
};
|
||||
|
||||
toString = () => {
|
||||
this.flushPlusMinus();
|
||||
this.flushKeeps();
|
||||
return this.assem.toString();
|
||||
};
|
||||
|
||||
clear = () => {
|
||||
this.minusAssem.clear();
|
||||
this.plusAssem.clear();
|
||||
this.keepAssem.clear();
|
||||
this.assem.clear();
|
||||
this.lengthChange = 0;
|
||||
};
|
||||
|
||||
endDocument = () => {
|
||||
this.keepAssem.endDocument();
|
||||
};
|
||||
|
||||
getLengthChange = () => this.lengthChange;
|
||||
}
|
18
src/static/js/StringAssembler.ts
Normal file
18
src/static/js/StringAssembler.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* @returns {StringAssembler}
|
||||
*/
|
||||
export class StringAssembler {
|
||||
private str = ''
|
||||
clear = ()=> {
|
||||
this.str = '';
|
||||
}
|
||||
/**
|
||||
* @param {string} x -
|
||||
*/
|
||||
append(x: string) {
|
||||
this.str += String(x);
|
||||
}
|
||||
toString() {
|
||||
return this.str
|
||||
}
|
||||
}
|
54
src/static/js/StringIterator.ts
Normal file
54
src/static/js/StringIterator.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import {assert} from "./Changeset";
|
||||
|
||||
/**
|
||||
* A custom made String Iterator
|
||||
*
|
||||
* @typedef {object} StringIterator
|
||||
* @property {Function} newlines -
|
||||
* @property {Function} peek -
|
||||
* @property {Function} remaining -
|
||||
* @property {Function} skip -
|
||||
* @property {Function} take -
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} str - String to iterate over
|
||||
* @returns {StringIterator}
|
||||
*/
|
||||
export class StringIterator {
|
||||
private curIndex: number;
|
||||
private newLines: number;
|
||||
private str: String
|
||||
|
||||
constructor(str: string) {
|
||||
this.curIndex = 0;
|
||||
this.str = str
|
||||
this.newLines = str.split('\n').length - 1;
|
||||
}
|
||||
remaining = () => this.str.length - this.curIndex;
|
||||
|
||||
getnewLines = () => this.newLines;
|
||||
|
||||
assertRemaining = (n: number) => {
|
||||
assert(n <= this.remaining(), `!(${n} <= ${this.remaining()})`);
|
||||
}
|
||||
|
||||
take = (n: number) => {
|
||||
this.assertRemaining(n);
|
||||
const s = this.str.substring(this.curIndex, this.curIndex+n);
|
||||
this.newLines -= s.split('\n').length - 1;
|
||||
this.curIndex += n;
|
||||
return s;
|
||||
}
|
||||
|
||||
peek = (n: number) => {
|
||||
this.assertRemaining(n);
|
||||
return this.str.substring(this.curIndex, this.curIndex+n);
|
||||
}
|
||||
|
||||
skip = (n: number) => {
|
||||
this.assertRemaining(n);
|
||||
this.curIndex += n;
|
||||
}
|
||||
|
||||
}
|
348
src/static/js/TextLinesMutator.ts
Normal file
348
src/static/js/TextLinesMutator.ts
Normal file
|
@ -0,0 +1,348 @@
|
|||
import {splitTextLines} from "./Changeset";
|
||||
|
||||
/**
|
||||
* Class to iterate and modify texts which have several lines. It is used for applying Changesets on
|
||||
* arrays of lines.
|
||||
*
|
||||
* Mutation operations have the same constraints as exports operations with respect to newlines, but
|
||||
* not the other additional constraints (i.e. ins/del ordering, forbidden no-ops, non-mergeability,
|
||||
* final newline). Can be used to mutate lists of strings where the last char of each string is not
|
||||
* actually a newline, but for the purposes of N and L values, the caller should pretend it is, and
|
||||
* for things to work right in that case, the input to the `insert` method should be a single line
|
||||
* with no newlines.
|
||||
*/
|
||||
class TextLinesMutator {
|
||||
private _lines: string[];
|
||||
private _curSplice: [number, number?];
|
||||
private _inSplice: boolean;
|
||||
private _curLine: number;
|
||||
private _curCol: number;
|
||||
/**
|
||||
* @param {(string[]|StringArrayLike)} lines - Lines to mutate (in place).
|
||||
*/
|
||||
constructor(lines: string[]) {
|
||||
this._lines = lines;
|
||||
/**
|
||||
* this._curSplice holds values that will be passed as arguments to this._lines.splice() to
|
||||
* insert, delete, or change lines:
|
||||
* - this._curSplice[0] is an index into the this._lines array.
|
||||
* - this._curSplice[1] is the number of lines that will be removed from the this._lines array
|
||||
* starting at the index.
|
||||
* - The other elements represent mutated (changed by ops) lines or new lines (added by ops)
|
||||
* to insert at the index.
|
||||
*
|
||||
* @type {[number, number?, ...string[]?]}
|
||||
*/
|
||||
this._curSplice = [0, 0];
|
||||
this._inSplice = false;
|
||||
// position in lines after curSplice is applied:
|
||||
this._curLine = 0;
|
||||
this._curCol = 0;
|
||||
// invariant: if (inSplice) then (curLine is in curSplice[0] + curSplice.length - {2,3}) &&
|
||||
// curLine >= curSplice[0]
|
||||
// invariant: if (inSplice && (curLine >= curSplice[0] + curSplice.length - 2)) then
|
||||
// curCol == 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a line from `lines` at given index.
|
||||
*
|
||||
* @param {number} idx - an index
|
||||
* @returns {string}
|
||||
*/
|
||||
_linesGet(idx: number) {
|
||||
if ('get' in this._lines) {
|
||||
// @ts-ignore
|
||||
return this._lines.get(idx) as string;
|
||||
} else {
|
||||
return this._lines[idx];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a slice from `lines`.
|
||||
*
|
||||
* @param {number} start - the start index
|
||||
* @param {number} end - the end index
|
||||
* @returns {string[]}
|
||||
*/
|
||||
_linesSlice(start: number | undefined, end: number | undefined) {
|
||||
// can be unimplemented if removeLines's return value not needed
|
||||
if (this._lines.slice) {
|
||||
return this._lines.slice(start, end);
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the length of `lines`.
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
_linesLength() {
|
||||
if (typeof this._lines.length === 'number') {
|
||||
return this._lines.length;
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return this._lines.length();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new splice.
|
||||
*/
|
||||
_enterSplice() {
|
||||
this._curSplice[0] = this._curLine;
|
||||
this._curSplice[1] = 0;
|
||||
// TODO(doc) when is this the case?
|
||||
// check all enterSplice calls and changes to curCol
|
||||
if (this._curCol > 0) this._putCurLineInSplice();
|
||||
this._inSplice = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the lines array according to the values in curSplice and resets curSplice. Called via
|
||||
* close or TODO(doc).
|
||||
*/
|
||||
_leaveSplice() {
|
||||
this._lines.splice(...this._curSplice);
|
||||
this._curSplice.length = 2;
|
||||
this._curSplice[0] = this._curSplice[1] = 0;
|
||||
this._inSplice = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if curLine is already in the splice. This is necessary because the last element in
|
||||
* curSplice is curLine when this line is currently worked on (e.g. when skipping or inserting).
|
||||
*
|
||||
* @returns {boolean} true if curLine is in splice
|
||||
*/
|
||||
_isCurLineInSplice() {
|
||||
// The value of `this._curSplice[1]` does not matter when determining the return value because
|
||||
// `this._curLine` refers to the line number *after* the splice is applied (so after those lines
|
||||
// are deleted).
|
||||
return this._curLine - this._curSplice[0] < this._curSplice.length - 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Incorporates current line into the splice and marks its old position to be deleted.
|
||||
*
|
||||
* @returns {number} the index of the added line in curSplice
|
||||
*/
|
||||
_putCurLineInSplice() {
|
||||
if (!this._isCurLineInSplice()) {
|
||||
// @ts-ignore
|
||||
this._curSplice.push(this._linesGet(this._curSplice[0] + this._curSplice[1]));
|
||||
// @ts-ignore
|
||||
this._curSplice[1]++;
|
||||
}
|
||||
// TODO should be the same as this._curSplice.length - 1
|
||||
return 2 + this._curLine - this._curSplice[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* It will skip some newlines by putting them into the splice.
|
||||
*
|
||||
* @param {number} L -
|
||||
* @param {boolean} includeInSplice - Indicates that attributes are present.
|
||||
*/
|
||||
skipLines(L: number, includeInSplice?: any) {
|
||||
if (!L) return;
|
||||
if (includeInSplice) {
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
// TODO(doc) should this count the number of characters that are skipped to check?
|
||||
for (let i = 0; i < L; i++) {
|
||||
this._curCol = 0;
|
||||
this._putCurLineInSplice();
|
||||
this._curLine++;
|
||||
}
|
||||
} else {
|
||||
if (this._inSplice) {
|
||||
if (L > 1) {
|
||||
// TODO(doc) figure out why single lines are incorporated into splice instead of ignored
|
||||
this._leaveSplice();
|
||||
} else {
|
||||
this._putCurLineInSplice();
|
||||
}
|
||||
}
|
||||
this._curLine += L;
|
||||
this._curCol = 0;
|
||||
}
|
||||
// tests case foo in remove(), which isn't otherwise covered in current impl
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip some characters. Can contain newlines.
|
||||
*
|
||||
* @param {number} N - number of characters to skip
|
||||
* @param {number} L - number of newlines to skip
|
||||
* @param {boolean} includeInSplice - indicates if attributes are present
|
||||
*/
|
||||
skip(N: number, L: number, includeInSplice?: any) {
|
||||
if (!N) return;
|
||||
if (L) {
|
||||
this.skipLines(L, includeInSplice);
|
||||
} else {
|
||||
if (includeInSplice && !this._inSplice) this._enterSplice();
|
||||
if (this._inSplice) {
|
||||
// although the line is put into splice curLine is not increased, because
|
||||
// only some chars are skipped, not the whole line
|
||||
this._putCurLineInSplice();
|
||||
}
|
||||
this._curCol += N;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove whole lines from lines array.
|
||||
*
|
||||
* @param {number} L - number of lines to remove
|
||||
* @returns {string}
|
||||
*/
|
||||
removeLines(L: number) {
|
||||
if (!L) return '';
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
|
||||
/**
|
||||
* Gets a string of joined lines after the end of the splice.
|
||||
*
|
||||
* @param {number} k - number of lines
|
||||
* @returns {string} joined lines
|
||||
*/
|
||||
const nextKLinesText = (k: number) => {
|
||||
// @ts-ignore
|
||||
const m = this._curSplice[0] + this._curSplice[1];
|
||||
return this._linesSlice(m, m + k).join('');
|
||||
};
|
||||
|
||||
let removed = '';
|
||||
if (this._isCurLineInSplice()) {
|
||||
if (this._curCol === 0) {
|
||||
// @ts-ignore
|
||||
removed = this._curSplice[this._curSplice.length - 1];
|
||||
this._curSplice.length--;
|
||||
removed += nextKLinesText(L - 1);
|
||||
// @ts-ignore
|
||||
this._curSplice[1] += L - 1;
|
||||
} else {
|
||||
removed = nextKLinesText(L - 1);
|
||||
// @ts-ignore
|
||||
this._curSplice[1] += L - 1;
|
||||
const sline = this._curSplice.length - 1;
|
||||
// @ts-ignore
|
||||
removed = this._curSplice[sline].substring(this._curCol) + removed;
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
|
||||
// @ts-ignore
|
||||
this._linesGet(this._curSplice[0] + this._curSplice[1]);
|
||||
// @ts-ignore
|
||||
this._curSplice[1] += 1;
|
||||
}
|
||||
} else {
|
||||
removed = nextKLinesText(L);
|
||||
this._curSplice[1]! += L;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove text from lines array.
|
||||
*
|
||||
* @param {number} N - characters to delete
|
||||
* @param {number} L - lines to delete
|
||||
* @returns {string}
|
||||
*/
|
||||
remove(N: number, L: any) {
|
||||
if (!N) return '';
|
||||
if (L) return this.removeLines(L);
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
// although the line is put into splice, curLine is not increased, because
|
||||
// only some chars are removed not the whole line
|
||||
const sline = this._putCurLineInSplice();
|
||||
// @ts-ignore
|
||||
const removed = this._curSplice[sline].substring(this._curCol, this._curCol + N);
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) +
|
||||
// @ts-ignore
|
||||
this._curSplice[sline].substring(this._curCol + N);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts text into lines array.
|
||||
*
|
||||
* @param {string} text - the text to insert
|
||||
* @param {number} L - number of newlines in text
|
||||
*/
|
||||
insert(text: string | any[], L: any) {
|
||||
if (!text) return;
|
||||
if (!this._inSplice) this._enterSplice();
|
||||
if (L) {
|
||||
// @ts-ignore
|
||||
const newLines = splitTextLines(text);
|
||||
if (this._isCurLineInSplice()) {
|
||||
const sline = this._curSplice.length - 1;
|
||||
/** @type {string} */
|
||||
const theLine = this._curSplice[sline];
|
||||
const lineCol = this._curCol;
|
||||
// Insert the chars up to `curCol` and the first new line.
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = theLine.substring(0, lineCol) + newLines[0];
|
||||
this._curLine++;
|
||||
newLines!.splice(0, 1);
|
||||
// insert the remaining new lines
|
||||
// @ts-ignore
|
||||
this._curSplice.push(...newLines);
|
||||
this._curLine += newLines!.length;
|
||||
// insert the remaining chars from the "old" line (e.g. the line we were in
|
||||
// when we started to insert new lines)
|
||||
// @ts-ignore
|
||||
this._curSplice.push(theLine.substring(lineCol));
|
||||
this._curCol = 0; // TODO(doc) why is this not set to the length of last line?
|
||||
} else {
|
||||
this._curSplice.push(...newLines);
|
||||
this._curLine += newLines!.length;
|
||||
}
|
||||
} else {
|
||||
// There are no additional lines. Although the line is put into splice, curLine is not
|
||||
// increased because there may be more chars in the line (newline is not reached).
|
||||
const sline = this._putCurLineInSplice();
|
||||
if (!this._curSplice[sline]) {
|
||||
const err = new Error(
|
||||
'curSplice[sline] not populated, actual curSplice contents is ' +
|
||||
`${JSON.stringify(this._curSplice)}. Possibly related to ` +
|
||||
'https://github.com/ether/etherpad-lite/issues/2802');
|
||||
console.error(err.stack || err.toString());
|
||||
}
|
||||
// @ts-ignore
|
||||
this._curSplice[sline] = this._curSplice[sline].substring(0, this._curCol) + text +
|
||||
// @ts-ignore
|
||||
this._curSplice[sline].substring(this._curCol);
|
||||
this._curCol += text.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if curLine (the line we are in when curSplice is applied) is the last line in `lines`.
|
||||
*
|
||||
* @returns {boolean} indicates if there are lines left
|
||||
*/
|
||||
hasMore() {
|
||||
let docLines = this._linesLength();
|
||||
if (this._inSplice) {
|
||||
// @ts-ignore
|
||||
docLines += this._curSplice.length - 2 - this._curSplice[1];
|
||||
}
|
||||
return this._curLine < docLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the splice
|
||||
*/
|
||||
close() {
|
||||
if (this._inSplice) this._leaveSplice();
|
||||
}
|
||||
}
|
||||
|
||||
export default TextLinesMutator
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-nocheck
|
||||
'use strict';
|
||||
import {Builder} from "./Builder";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
|
@ -24,6 +24,8 @@ const browser = require('./vendors/browser');
|
|||
import padutils from './pad_utils'
|
||||
const Ace2Common = require('./ace2_common');
|
||||
const $ = require('./rjquery').$;
|
||||
import {characterRangeFollow, checkRep, cloneAText, compose, deserializeOps, filterAttribNumbers, inverse, isIdentity, makeAText, makeAttribution, mapAttribNumbers, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, oldLen, opsFromAText, pack, splitAttributionLines} from './Changeset'
|
||||
|
||||
|
||||
const isNodeText = Ace2Common.isNodeText;
|
||||
const getAssoc = Ace2Common.getAssoc;
|
||||
|
@ -33,14 +35,15 @@ const hooks = require('./pluginfw/hooks');
|
|||
import SkipList from "./skiplist";
|
||||
import Scroll from './scroll'
|
||||
import AttribPool from './AttributePool'
|
||||
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||
import Op from "./Op";
|
||||
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils'
|
||||
|
||||
function Ace2Inner(editorInfo, cssManagers) {
|
||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||
const domline = require('./domline').domline;
|
||||
const Changeset = require('./Changeset');
|
||||
const ChangesetUtils = require('./ChangesetUtils');
|
||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
const undoModule = require('./undomodule').undoModule;
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
|
@ -174,9 +177,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// CCCCCCCCCCCCCCCCCCCC\n
|
||||
// CCCC\n
|
||||
// end[0]: <CCC end[1] CCC>-------\n
|
||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
||||
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
|
||||
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
|
||||
const builder = new Builder(rep.lines.totalWidth());
|
||||
buildKeepToStartOfRange(rep, builder, start);
|
||||
buildRemoveRange(rep, builder, start, end);
|
||||
builder.insert(newText, [
|
||||
['author', thisAuthor],
|
||||
], rep.apool);
|
||||
|
@ -495,10 +498,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const importAText = (atext, apoolJsonObj, undoable) => {
|
||||
atext = Changeset.cloneAText(atext);
|
||||
atext = cloneAText(atext);
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttribPool()).fromJsonable(apoolJsonObj);
|
||||
atext.attribs = Changeset.moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
|
||||
atext.attribs = moveOpsToNewPool(atext.attribs, wireApool, rep.apool);
|
||||
}
|
||||
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||
setDocAText(atext);
|
||||
|
@ -527,18 +530,18 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const numLines = rep.lines.length();
|
||||
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
||||
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
||||
const assem = Changeset.smartOpAssembler();
|
||||
const o = new Changeset.Op('-');
|
||||
const assem = new SmartOpAssembler();
|
||||
const o = new Op('-');
|
||||
o.chars = upToLastLine;
|
||||
o.lines = numLines - 1;
|
||||
assem.append(o);
|
||||
o.chars = lastLineLength;
|
||||
o.lines = 0;
|
||||
assem.append(o);
|
||||
for (const op of Changeset.opsFromAText(atext)) assem.append(op);
|
||||
for (const op of opsFromAText(atext)) assem.append(op);
|
||||
const newLen = oldLen + assem.getLengthChange();
|
||||
const changeset = Changeset.checkRep(
|
||||
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
||||
const changeset = checkRep(
|
||||
pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
||||
performDocumentApplyChangeset(changeset);
|
||||
|
||||
performSelectionChange(
|
||||
|
@ -552,7 +555,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const setDocText = (text) => {
|
||||
setDocAText(Changeset.makeAText(text));
|
||||
setDocAText(makeAText(text));
|
||||
};
|
||||
|
||||
const getDocText = () => {
|
||||
|
@ -1271,7 +1274,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
||||
theIndent += THE_TAB;
|
||||
}
|
||||
const cs = Changeset.builder(rep.lines.totalWidth()).keep(
|
||||
const cs = new Builder(rep.lines.totalWidth()).keep(
|
||||
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
|
||||
theIndent, [
|
||||
['author', thisAuthor],
|
||||
|
@ -1423,7 +1426,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
|
||||
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
|
||||
const result =
|
||||
Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
||||
characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
||||
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
|
||||
}
|
||||
|
||||
|
@ -1435,7 +1438,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
length: () => rep.lines.length(),
|
||||
};
|
||||
|
||||
Changeset.mutateTextLines(changes, linesMutatee);
|
||||
mutateTextLines(changes, linesMutatee);
|
||||
|
||||
if (requiredSelectionSetting) {
|
||||
performSelectionChange(
|
||||
|
@ -1446,10 +1449,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
};
|
||||
|
||||
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
|
||||
Changeset.checkRep(changes);
|
||||
checkRep(changes);
|
||||
|
||||
if (Changeset.oldLen(changes) !== rep.alltext.length) {
|
||||
const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`;
|
||||
if (oldLen(changes) !== rep.alltext.length) {
|
||||
const errMsg = `${oldLen(changes)}/${rep.alltext.length}`;
|
||||
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
|
||||
}
|
||||
|
||||
|
@ -1458,10 +1461,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (!editEvent.changeset) {
|
||||
editEvent.changeset = changes;
|
||||
} else {
|
||||
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
|
||||
editEvent.changeset = compose(editEvent.changeset, changes, rep.apool);
|
||||
}
|
||||
} else {
|
||||
const inverseChangeset = Changeset.inverse(changes, {
|
||||
const inverseChangeset = inverse(changes, {
|
||||
get: (i) => `${rep.lines.atIndex(i).text}\n`,
|
||||
length: () => rep.lines.length(),
|
||||
}, rep.alines, rep.apool);
|
||||
|
@ -1469,11 +1472,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (!editEvent.backset) {
|
||||
editEvent.backset = inverseChangeset;
|
||||
} else {
|
||||
editEvent.backset = Changeset.compose(inverseChangeset, editEvent.backset, rep.apool);
|
||||
editEvent.backset = compose(inverseChangeset, editEvent.backset, rep.apool);
|
||||
}
|
||||
}
|
||||
|
||||
Changeset.mutateAttributionLines(changes, rep.alines, rep.apool);
|
||||
mutateAttributionLines(changes, rep.alines, rep.apool);
|
||||
|
||||
if (changesetTracker.isTracking()) {
|
||||
changesetTracker.composeUserChangeset(changes);
|
||||
|
@ -1582,7 +1585,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
let hasAttrib = true;
|
||||
|
||||
let indexIntoLine = 0;
|
||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
||||
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||
const opStartInLine = indexIntoLine;
|
||||
const opEndInLine = opStartInLine + op.chars;
|
||||
if (!hasIt(op.attribs)) {
|
||||
|
@ -1627,7 +1630,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (n === selEndLine) {
|
||||
selectionEndInLine = rep.selEnd[1];
|
||||
}
|
||||
for (const op of Changeset.deserializeOps(rep.alines[n])) {
|
||||
for (const op of deserializeOps(rep.alines[n])) {
|
||||
const opStartInLine = indexIntoLine;
|
||||
const opEndInLine = opStartInLine + op.chars;
|
||||
if (!hasIt(op.attribs)) {
|
||||
|
@ -1745,7 +1748,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
|
||||
|
||||
const startBuilder = () => {
|
||||
const builder = Changeset.builder(oldLen);
|
||||
const builder = new Builder(oldLen);
|
||||
builder.keep(spliceStartLineStart, spliceStartLine);
|
||||
builder.keep(spliceStart - spliceStartLineStart);
|
||||
return builder;
|
||||
|
@ -1755,7 +1758,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
let textIndex = 0;
|
||||
const newTextStart = commonStart;
|
||||
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
|
||||
for (const op of Changeset.deserializeOps(attribs)) {
|
||||
for (const op of deserializeOps(attribs)) {
|
||||
const nextIndex = textIndex + op.chars;
|
||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
||||
|
@ -1773,7 +1776,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// changeset the applies the styles found in the DOM.
|
||||
// This allows us to incorporate, e.g., Safari's native "unbold".
|
||||
const incorpedAttribClearer = cachedStrFunc(
|
||||
(oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
|
||||
(oldAtts) => mapAttribNumbers(oldAtts, (n) => {
|
||||
const k = rep.apool.getAttribKey(n);
|
||||
if (isStyleAttribute(k)) {
|
||||
return rep.apool.putAttrib([k, '']);
|
||||
|
@ -1799,7 +1802,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
});
|
||||
const styler = builder2.toString();
|
||||
|
||||
theChangeset = Changeset.compose(clearer, styler, rep.apool);
|
||||
theChangeset = compose(clearer, styler, rep.apool);
|
||||
} else {
|
||||
const builder = startBuilder();
|
||||
|
||||
|
@ -1869,7 +1872,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const attribRuns = (attribs) => {
|
||||
const lengs = [];
|
||||
const atts = [];
|
||||
for (const op of Changeset.deserializeOps(attribs)) {
|
||||
for (const op of deserializeOps(attribs)) {
|
||||
lengs.push(op.chars);
|
||||
atts.push(op.attribs);
|
||||
}
|
||||
|
@ -1898,8 +1901,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const newLen = newText.length;
|
||||
const minLen = Math.min(oldLen, newLen);
|
||||
|
||||
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
||||
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
||||
const oldARuns = attribRuns(filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
||||
const newARuns = attribRuns(filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
||||
|
||||
let commonStart = 0;
|
||||
const oldStartIter = attribIterator(oldARuns, false);
|
||||
|
@ -2297,7 +2300,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
// 3-renumber every list item of the same level from the beginning, level 1
|
||||
// IMPORTANT: never skip a level because there imbrication may be arbitrary
|
||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
||||
const builder = new Builder(rep.lines.totalWidth());
|
||||
let loc = [0, 0];
|
||||
const applyNumberList = (line, level) => {
|
||||
// init
|
||||
|
@ -2312,8 +2315,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
if (isNaN(curLevel) || listType[0] === 'indent') {
|
||||
return line;
|
||||
} else if (curLevel === level) {
|
||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
||||
buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
||||
buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
||||
['start', position],
|
||||
], rep.apool);
|
||||
|
||||
|
@ -2330,7 +2333,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
|
||||
applyNumberList(lineNum, 1);
|
||||
const cs = builder.toString();
|
||||
if (!Changeset.isIdentity(cs)) {
|
||||
if (!isIdentity(cs)) {
|
||||
performDocumentApplyChangeset(cs);
|
||||
}
|
||||
|
||||
|
@ -2618,7 +2621,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
// TODO: There appears to be a race condition or so.
|
||||
const authorIds = new Set();
|
||||
if (alineAttrs) {
|
||||
for (const op of Changeset.deserializeOps(alineAttrs)) {
|
||||
for (const op of deserializeOps(alineAttrs)) {
|
||||
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
|
||||
if (authorId) authorIds.add(authorId);
|
||||
}
|
||||
|
@ -3513,8 +3516,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
|||
const oneEntry = createDomLineEntry('');
|
||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||
insertDomLines(null, [oneEntry.domInfo]);
|
||||
rep.alines = Changeset.splitAttributionLines(
|
||||
Changeset.makeAttribution('\n'), '\n');
|
||||
rep.alines = splitAttributionLines(
|
||||
makeAttribution('\n'), '\n');
|
||||
|
||||
bindTheEventHandlers();
|
||||
});
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||
const domline = require('./domline').domline;
|
||||
import AttribPool from './AttributePool';
|
||||
const Changeset = require('./Changeset');
|
||||
import {compose, deserializeOps, inverse, moveOpsToNewPool, mutateAttributionLines, mutateTextLines, splitAttributionLines, splitTextLines, unpack} from './Changeset';
|
||||
const attributes = require('./attributes');
|
||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||
const colorutils = require('./colorutils').colorutils;
|
||||
|
@ -54,11 +54,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
currentRevision: clientVars.collab_client_vars.rev,
|
||||
currentTime: clientVars.collab_client_vars.time,
|
||||
currentLines:
|
||||
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||
splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||
currentDivs: null,
|
||||
// to be filled in once the dom loads
|
||||
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
|
||||
alines: Changeset.splitAttributionLines(
|
||||
alines: splitAttributionLines(
|
||||
clientVars.collab_client_vars.initialAttributedText.attribs,
|
||||
clientVars.collab_client_vars.initialAttributedText.text),
|
||||
|
||||
|
@ -121,7 +121,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
getActiveAuthors() {
|
||||
const authorIds = new Set();
|
||||
for (const aline of this.alines) {
|
||||
for (const op of Changeset.deserializeOps(aline)) {
|
||||
for (const op of deserializeOps(aline)) {
|
||||
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
|
||||
if (k !== 'author') continue;
|
||||
if (v) authorIds.add(v);
|
||||
|
@ -142,7 +142,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
const oldAlines = padContents.alines.slice();
|
||||
try {
|
||||
// must mutate attribution lines before text lines
|
||||
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||
mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
// some chars are replaced (no attributes change and no length change)
|
||||
// test if there are keep ops at the start of the cs
|
||||
if (lineChanged === undefined) {
|
||||
const [op] = Changeset.deserializeOps(Changeset.unpack(changeset).ops);
|
||||
const [op] = deserializeOps(unpack(changeset).ops);
|
||||
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
|
||||
}
|
||||
|
||||
|
@ -184,7 +184,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
goToLineNumber(lineChanged);
|
||||
}
|
||||
|
||||
Changeset.mutateTextLines(changeset, padContents);
|
||||
mutateTextLines(changeset, padContents);
|
||||
padContents.currentRevision = revision;
|
||||
padContents.currentTime += timeDelta * 1000;
|
||||
|
||||
|
@ -273,7 +273,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
let changeset = cs[0];
|
||||
let timeDelta = path.times[0];
|
||||
for (let i = 1; i < cs.length; i++) {
|
||||
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
|
||||
changeset = compose(changeset, cs[i], padContents.apool);
|
||||
timeDelta += path.times[i];
|
||||
}
|
||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||
|
@ -291,7 +291,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
let changeset = cs[0];
|
||||
let timeDelta = path.times[0];
|
||||
for (let i = 1; i < cs.length; i++) {
|
||||
changeset = Changeset.compose(changeset, cs[i], padContents.apool);
|
||||
changeset = compose(changeset, cs[i], padContents.apool);
|
||||
timeDelta += path.times[i];
|
||||
}
|
||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||
|
@ -397,9 +397,9 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
|
||||
// debugLog("adding changeset:", astart, aend);
|
||||
const forwardcs =
|
||||
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||
moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||
const backwardcs =
|
||||
Changeset.moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
|
||||
moveOpsToNewPool(data.backwardsChangesets[i], pool, padContents.apool);
|
||||
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
|
||||
}
|
||||
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
||||
|
@ -409,13 +409,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
|||
obj = obj.data;
|
||||
|
||||
if (obj.type === 'NEW_CHANGES') {
|
||||
const changeset = Changeset.moveOpsToNewPool(
|
||||
const changeset = moveOpsToNewPool(
|
||||
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
let changesetBack = Changeset.inverse(
|
||||
let changesetBack = inverse(
|
||||
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
||||
|
||||
changesetBack = Changeset.moveOpsToNewPool(
|
||||
changesetBack = moveOpsToNewPool(
|
||||
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||
|
||||
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
||||
|
|
|
@ -25,15 +25,16 @@
|
|||
|
||||
import AttributeMap from './AttributeMap';
|
||||
import AttributePool from './AttributePool';
|
||||
const Changeset = require('./Changeset');
|
||||
import {applyToAText, checkRep, cloneAText, compose, deserializeOps, follow, identity, isIdentity, makeAText, moveOpsToNewPool, newLen, pack, prepareForWire, unpack} from './Changeset';
|
||||
import {MergingOpAssembler} from "./MergingOpAssembler";
|
||||
|
||||
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||
// latest official text from server
|
||||
let baseAText = Changeset.makeAText('\n');
|
||||
let baseAText = makeAText('\n');
|
||||
// changes applied to baseText that have been submitted
|
||||
let submittedChangeset = null;
|
||||
// changes applied to submittedChangeset since it was prepared
|
||||
let userChangeset = Changeset.identity(1);
|
||||
let userChangeset = identity(1);
|
||||
// is the changesetTracker enabled
|
||||
let tracking = false;
|
||||
// stack state flag so that when we change the rep we don't
|
||||
|
@ -67,18 +68,18 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
return self = {
|
||||
isTracking: () => tracking,
|
||||
setBaseText: (text) => {
|
||||
self.setBaseAttributedText(Changeset.makeAText(text), null);
|
||||
self.setBaseAttributedText(makeAText(text), null);
|
||||
},
|
||||
setBaseAttributedText: (atext, apoolJsonObj) => {
|
||||
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
|
||||
tracking = true;
|
||||
baseAText = Changeset.cloneAText(atext);
|
||||
baseAText = cloneAText(atext);
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||
baseAText.attribs = moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||
}
|
||||
submittedChangeset = null;
|
||||
userChangeset = Changeset.identity(atext.text.length);
|
||||
userChangeset = identity(atext.text.length);
|
||||
applyingNonUserChanges = true;
|
||||
try {
|
||||
callbacks.setDocumentAttributedText(atext);
|
||||
|
@ -90,8 +91,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
composeUserChangeset: (c) => {
|
||||
if (!tracking) return;
|
||||
if (applyingNonUserChanges) return;
|
||||
if (Changeset.isIdentity(c)) return;
|
||||
userChangeset = Changeset.compose(userChangeset, c, apool);
|
||||
if (isIdentity(c)) return;
|
||||
userChangeset = compose(userChangeset, c, apool);
|
||||
|
||||
setChangeCallbackTimeout();
|
||||
},
|
||||
|
@ -101,23 +102,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
|
||||
if (apoolJsonObj) {
|
||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||
c = Changeset.moveOpsToNewPool(c, wireApool, apool);
|
||||
c = moveOpsToNewPool(c, wireApool, apool);
|
||||
}
|
||||
|
||||
baseAText = Changeset.applyToAText(c, baseAText, apool);
|
||||
baseAText = applyToAText(c, baseAText, apool);
|
||||
|
||||
let c2 = c;
|
||||
if (submittedChangeset) {
|
||||
const oldSubmittedChangeset = submittedChangeset;
|
||||
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
|
||||
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
|
||||
submittedChangeset = follow(c, oldSubmittedChangeset, false, apool);
|
||||
c2 = follow(oldSubmittedChangeset, c, true, apool);
|
||||
}
|
||||
|
||||
const preferInsertingAfterUserChanges = true;
|
||||
const oldUserChangeset = userChangeset;
|
||||
userChangeset = Changeset.follow(
|
||||
userChangeset = follow(
|
||||
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
||||
const postChange = Changeset.follow(
|
||||
const postChange = follow(
|
||||
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
||||
|
||||
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
|
||||
|
@ -136,17 +137,17 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
if (submittedChangeset) {
|
||||
// submission must have been canceled, prepare new changeset
|
||||
// that includes old submittedChangeset
|
||||
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
|
||||
toSubmit = compose(submittedChangeset, userChangeset, apool);
|
||||
} else {
|
||||
// Get my authorID
|
||||
const authorId = parent.parent.pad.myUserInfo.userId;
|
||||
|
||||
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
||||
// text was copied from another author.
|
||||
const cs = Changeset.unpack(userChangeset);
|
||||
const assem = Changeset.mergingOpAssembler();
|
||||
const cs = unpack(userChangeset);
|
||||
const assem = new MergingOpAssembler();
|
||||
|
||||
for (const op of Changeset.deserializeOps(cs.ops)) {
|
||||
for (const op of deserializeOps(cs.ops)) {
|
||||
if (op.opcode === '+') {
|
||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||
const oldAuthorId = attribs.get('author');
|
||||
|
@ -158,23 +159,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
assem.append(op);
|
||||
}
|
||||
assem.endDocument();
|
||||
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
||||
Changeset.checkRep(userChangeset);
|
||||
userChangeset = pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
||||
checkRep(userChangeset);
|
||||
|
||||
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
|
||||
if (isIdentity(userChangeset)) toSubmit = null;
|
||||
else toSubmit = userChangeset;
|
||||
}
|
||||
|
||||
let cs = null;
|
||||
if (toSubmit) {
|
||||
submittedChangeset = toSubmit;
|
||||
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
|
||||
userChangeset = identity(newLen(toSubmit));
|
||||
|
||||
cs = toSubmit;
|
||||
}
|
||||
let wireApool = null;
|
||||
if (cs) {
|
||||
const forWire = Changeset.prepareForWire(cs, apool);
|
||||
const forWire = prepareForWire(cs, apool);
|
||||
wireApool = forWire.pool.toJsonable();
|
||||
cs = forWire.translated;
|
||||
}
|
||||
|
@ -191,13 +192,13 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
|||
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
|
||||
}
|
||||
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
||||
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
|
||||
baseAText = applyToAText(submittedChangeset, baseAText, apool);
|
||||
submittedChangeset = null;
|
||||
},
|
||||
setUserChangeNotificationCallback: (callback) => {
|
||||
changeCallback = callback;
|
||||
},
|
||||
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
|
||||
hasUncommittedChanges: () => !!(submittedChangeset || (!isIdentity(userChangeset))),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @ts-nocheck
|
||||
|
||||
'use strict';
|
||||
/**
|
||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
||||
|
@ -9,6 +10,8 @@
|
|||
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
|
||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||
// %APPJET%: import("etherpad.admin.plugins");
|
||||
import Op from "./Op";
|
||||
|
||||
/**
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
|
@ -28,8 +31,9 @@
|
|||
const _MAX_LIST_LEVEL = 16;
|
||||
|
||||
import AttributeMap from './AttributeMap';
|
||||
const UNorm = require('unorm');
|
||||
const Changeset = require('./Changeset');
|
||||
import UNorm from 'unorm';
|
||||
import {subattribution} from './Changeset';
|
||||
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
|
||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||
|
@ -84,14 +88,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
const textArray = [];
|
||||
const attribsArray = [];
|
||||
let attribsBuilder = null;
|
||||
const op = new Changeset.Op('+');
|
||||
const op = new Op('+');
|
||||
const self = {
|
||||
length: () => textArray.length,
|
||||
atColumnZero: () => textArray[textArray.length - 1] === '',
|
||||
startNew: () => {
|
||||
textArray.push('');
|
||||
self.flush(true);
|
||||
attribsBuilder = Changeset.smartOpAssembler();
|
||||
attribsBuilder = new SmartOpAssembler();
|
||||
},
|
||||
textOfLine: (i) => textArray[i],
|
||||
appendText: (txt, attrString = '') => {
|
||||
|
@ -654,8 +658,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
|||
const lengthToTake = lineLimit;
|
||||
newStrings.push(oldString.substring(0, lengthToTake));
|
||||
oldString = oldString.substring(lengthToTake);
|
||||
newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
|
||||
oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
|
||||
newAttribStrings.push(subattribution(oldAttribString, 0, lengthToTake));
|
||||
oldAttribString = subattribution(oldAttribString, lengthToTake);
|
||||
}
|
||||
if (oldString.length > 0) {
|
||||
newStrings.push(oldString);
|
||||
|
|
|
@ -31,12 +31,13 @@
|
|||
// requires: plugins
|
||||
// requires: undefined
|
||||
|
||||
const Changeset = require('./Changeset');
|
||||
const attributes = require('./attributes');
|
||||
import {deserializeOps} from './Changeset';
|
||||
import attributes from './attributes';
|
||||
const hooks = require('./pluginfw/hooks');
|
||||
const linestylefilter = {};
|
||||
const AttributeManager = require('./AttributeManager');
|
||||
import padutils from './pad_utils'
|
||||
import Op from "./Op";
|
||||
|
||||
linestylefilter.ATTRIB_CLASSES = {
|
||||
bold: 'tag:b',
|
||||
|
@ -99,12 +100,12 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
|||
return classes.substring(1);
|
||||
};
|
||||
|
||||
const attrOps = Changeset.deserializeOps(aline);
|
||||
const attrOps = deserializeOps(aline);
|
||||
let attrOpsNext = attrOps.next();
|
||||
let nextOp, nextOpClasses;
|
||||
|
||||
const goNextOp = () => {
|
||||
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
|
||||
nextOp = attrOpsNext.done ? new Op() : attrOpsNext.value;
|
||||
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
|
||||
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
|
||||
};
|
||||
|
|
6
src/static/js/types/ChangeSet.ts
Normal file
6
src/static/js/types/ChangeSet.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export type ChangeSet = {
|
||||
oldLen: number,
|
||||
newLen: number,
|
||||
ops: string
|
||||
charBank: string
|
||||
}
|
7
src/static/js/types/ChangeSetBuilder.ts
Normal file
7
src/static/js/types/ChangeSetBuilder.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import {Attribute} from "./Attribute";
|
||||
import AttributePool from "../AttributePool";
|
||||
|
||||
export type ChangeSetBuilder = {
|
||||
remove: (start: number, end?: number)=>void,
|
||||
keep: (start: number, end?: number, attribs?: Attribute[], pool?: AttributePool)=>void
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Changeset = require('./Changeset');
|
||||
import {characterRangeFollow, compose, follow, isIdentity, unpack} from './Changeset';
|
||||
const _ = require('./underscore');
|
||||
|
||||
const undoModule = (() => {
|
||||
|
@ -62,7 +62,7 @@ const undoModule = (() => {
|
|||
const idx = stackElements.length - 1;
|
||||
if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
|
||||
stackElements[idx].changeset =
|
||||
Changeset.compose(stackElements[idx].changeset, cs, getAPool());
|
||||
compose(stackElements[idx].changeset, cs, getAPool());
|
||||
} else {
|
||||
stackElements.push(
|
||||
{
|
||||
|
@ -83,10 +83,10 @@ const undoModule = (() => {
|
|||
if (un.backset) {
|
||||
const excs = ex.changeset;
|
||||
const unbs = un.backset;
|
||||
un.backset = Changeset.follow(excs, un.backset, false, getAPool());
|
||||
ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool());
|
||||
un.backset = follow(excs, un.backset, false, getAPool());
|
||||
ex.changeset = follow(unbs, ex.changeset, true, getAPool());
|
||||
if ((typeof un.selStart) === 'number') {
|
||||
const newSel = Changeset.characterRangeFollow(excs, un.selStart, un.selEnd);
|
||||
const newSel = characterRangeFollow(excs, un.selStart, un.selEnd);
|
||||
un.selStart = newSel[0];
|
||||
un.selEnd = newSel[1];
|
||||
if (un.selStart === un.selEnd) {
|
||||
|
@ -98,7 +98,7 @@ const undoModule = (() => {
|
|||
stackElements[idx] = un;
|
||||
if (idx >= 2 && stackElements[idx - 2].elementType === EXTERNAL_CHANGE) {
|
||||
ex.changeset =
|
||||
Changeset.compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
|
||||
compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
|
||||
stackElements.splice(idx - 2, 1);
|
||||
idx--;
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ const undoModule = (() => {
|
|||
return count;
|
||||
};
|
||||
|
||||
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(Changeset.unpack(cs).ops, opcode);
|
||||
const _opcodeOccurrences = (cs, opcode) => _charOccurrences(unpack(cs).ops, opcode);
|
||||
|
||||
const _mergeChangesets = (cs1, cs2) => {
|
||||
if (!cs1) return cs2;
|
||||
|
@ -171,14 +171,14 @@ const undoModule = (() => {
|
|||
const minusCount1 = _opcodeOccurrences(cs1, '-');
|
||||
const minusCount2 = _opcodeOccurrences(cs2, '-');
|
||||
if (plusCount1 === 1 && plusCount2 === 1 && minusCount1 === 0 && minusCount2 === 0) {
|
||||
const merge = Changeset.compose(cs1, cs2, getAPool());
|
||||
const merge = compose(cs1, cs2, getAPool()!);
|
||||
const plusCount3 = _opcodeOccurrences(merge, '+');
|
||||
const minusCount3 = _opcodeOccurrences(merge, '-');
|
||||
if (plusCount3 === 1 && minusCount3 === 0) {
|
||||
return merge;
|
||||
}
|
||||
} else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
|
||||
const merge = Changeset.compose(cs1, cs2, getAPool());
|
||||
const merge = compose(cs1, cs2, getAPool()!);
|
||||
const plusCount3 = _opcodeOccurrences(merge, '+');
|
||||
const minusCount3 = _opcodeOccurrences(merge, '-');
|
||||
if (plusCount3 === 0 && minusCount3 === 1) {
|
||||
|
@ -199,7 +199,7 @@ const undoModule = (() => {
|
|||
}
|
||||
};
|
||||
|
||||
if ((!event.backset) || Changeset.isIdentity(event.backset)) {
|
||||
if ((!event.backset) || isIdentity(event.backset)) {
|
||||
applySelectionToTop();
|
||||
} else {
|
||||
let merged = false;
|
||||
|
@ -227,7 +227,7 @@ const undoModule = (() => {
|
|||
};
|
||||
|
||||
const reportExternalChange = (changeset) => {
|
||||
if (changeset && !Changeset.isIdentity(changeset)) {
|
||||
if (changeset && !isIdentity(changeset)) {
|
||||
stack.pushExternalChange(changeset);
|
||||
}
|
||||
};
|
||||
|
|
220
src/tests/backend-new/easysync-helper.ts
Normal file
220
src/tests/backend-new/easysync-helper.ts
Normal 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];
|
||||
};
|
47
src/tests/backend-new/specs/StringIteratorTest.ts
Normal file
47
src/tests/backend-new/specs/StringIteratorTest.ts
Normal 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');
|
||||
});
|
||||
})
|
224
src/tests/backend-new/specs/easysync-assembler.ts
Normal file
224
src/tests/backend-new/specs/easysync-assembler.ts
Normal 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');
|
||||
});
|
||||
});
|
54
src/tests/backend-new/specs/easysync-compose.ts
Normal file
54
src/tests/backend-new/specs/easysync-compose.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
320
src/tests/backend-new/specs/easysync-mutations.ts
Normal file
320
src/tests/backend-new/specs/easysync-mutations.ts
Normal 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',
|
||||
]);
|
||||
});
|
||||
});
|
166
src/tests/backend-new/specs/easysync-other.test.ts
Normal file
166
src/tests/backend-new/specs/easysync-other.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
55
src/tests/backend-new/specs/easysync-subAttribution.ts
Normal file
55
src/tests/backend-new/specs/easysync-subAttribution.ts
Normal 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');
|
||||
});
|
|
@ -2,6 +2,6 @@ import { defineConfig } from 'vitest/config'
|
|||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ["tests/backend-new/**/*.ts"],
|
||||
include: ["tests/backend-new/specs/**/*.ts"],
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue