mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-20 15:36:16 -04:00
Feat/changeset ts (#6594)
* Migrated changeset * Added more tests. * Fixed test scopes
This commit is contained in:
parent
3dae23a1e5
commit
28e04bdf71
37 changed files with 2540 additions and 1310 deletions
|
@ -19,8 +19,10 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps} from '../../static/js/Changeset';
|
||||||
import ChatMessage from '../../static/js/ChatMessage';
|
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 CustomError = require('../utils/customError');
|
||||||
const padManager = require('./PadManager');
|
const padManager = require('./PadManager');
|
||||||
const padMessageHandler = require('../handler/PadMessageHandler');
|
const padMessageHandler = require('../handler/PadMessageHandler');
|
||||||
|
@ -563,11 +565,11 @@ exports.restoreRevision = async (padID: string, rev: number, authorId = '') => {
|
||||||
const oldText = pad.text();
|
const oldText = pad.text();
|
||||||
atext.text += '\n';
|
atext.text += '\n';
|
||||||
|
|
||||||
const eachAttribRun = (attribs: string[], func:Function) => {
|
const eachAttribRun = (attribs: string, func:Function) => {
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = 0;
|
const newTextStart = 0;
|
||||||
const newTextEnd = atext.text.length;
|
const newTextEnd = atext.text.length;
|
||||||
for (const op of Changeset.deserializeOps(attribs)) {
|
for (const op of deserializeOps(attribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
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
|
// 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
|
// 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);
|
builder.insert(atext.text.substring(start, end), attribs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {MapArrayType} from "../types/MapType";
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
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 ChatMessage from '../../static/js/ChatMessage';
|
||||||
import AttributePool from '../../static/js/AttributePool';
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const Stream = require('../utils/Stream');
|
const Stream = require('../utils/Stream');
|
||||||
|
@ -24,6 +24,7 @@ const readOnlyManager = require('./ReadOnlyManager');
|
||||||
const randomString = require('../utils/randomstring');
|
const randomString = require('../utils/randomstring');
|
||||||
const hooks = require('../../static/js/pluginfw/hooks');
|
const hooks = require('../../static/js/pluginfw/hooks');
|
||||||
import pad_utils from "../../static/js/pad_utils";
|
import pad_utils from "../../static/js/pad_utils";
|
||||||
|
import {SmartOpAssembler} from "../../static/js/SmartOpAssembler";
|
||||||
const promises = require('../utils/promises');
|
const promises = require('../utils/promises');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,7 +57,7 @@ class Pad {
|
||||||
*/
|
*/
|
||||||
constructor(id:string, database = db) {
|
constructor(id:string, database = db) {
|
||||||
this.db = database;
|
this.db = database;
|
||||||
this.atext = Changeset.makeAText('\n');
|
this.atext = makeAText('\n');
|
||||||
this.pool = new AttributePool();
|
this.pool = new AttributePool();
|
||||||
this.head = -1;
|
this.head = -1;
|
||||||
this.chatHead = -1;
|
this.chatHead = -1;
|
||||||
|
@ -93,13 +94,13 @@ class Pad {
|
||||||
* @param {String} authorId The id of the author
|
* @param {String} authorId The id of the author
|
||||||
* @return {Promise<number|string>}
|
* @return {Promise<number|string>}
|
||||||
*/
|
*/
|
||||||
async appendRevision(aChangeset:AChangeSet, authorId = '') {
|
async appendRevision(aChangeset:string, authorId = '') {
|
||||||
const newAText = Changeset.applyToAText(aChangeset, this.atext, this.pool);
|
const newAText = applyToAText(aChangeset, this.atext, this.pool);
|
||||||
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
if (newAText.text === this.atext.text && newAText.attribs === this.atext.attribs &&
|
||||||
this.head !== -1) {
|
this.head !== -1) {
|
||||||
return this.head;
|
return this.head;
|
||||||
}
|
}
|
||||||
Changeset.copyAText(newAText, this.atext);
|
copyAText(newAText, this.atext);
|
||||||
|
|
||||||
const newRev = ++this.head;
|
const newRev = ++this.head;
|
||||||
|
|
||||||
|
@ -215,7 +216,7 @@ class Pad {
|
||||||
]);
|
]);
|
||||||
const apool = this.apool();
|
const apool = this.apool();
|
||||||
let atext = keyAText;
|
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;
|
return atext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,7 +294,7 @@ class Pad {
|
||||||
(!ins && start > 0 && orig[start - 1] === '\n');
|
(!ins && start > 0 && orig[start - 1] === '\n');
|
||||||
if (!willEndWithNewline) ins += '\n';
|
if (!willEndWithNewline) ins += '\n';
|
||||||
if (ndel === 0 && ins.length === 0) return;
|
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);
|
await this.appendRevision(changeset, authorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,7 +394,7 @@ class Pad {
|
||||||
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
if (context.type !== 'text') throw new Error(`unsupported content type: ${context.type}`);
|
||||||
text = exports.cleanText(context.content);
|
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 this.appendRevision(firstChangeset, authorId);
|
||||||
}
|
}
|
||||||
await hooks.aCallAll('padLoad', {pad: this});
|
await hooks.aCallAll('padLoad', {pad: this});
|
||||||
|
@ -520,8 +521,8 @@ class Pad {
|
||||||
const oldAText = this.atext;
|
const oldAText = this.atext;
|
||||||
|
|
||||||
// based on Changeset.makeSplice
|
// based on Changeset.makeSplice
|
||||||
const assem = Changeset.smartOpAssembler();
|
const assem = new SmartOpAssembler();
|
||||||
for (const op of Changeset.opsFromAText(oldAText)) assem.append(op);
|
for (const op of opsFromAText(oldAText)) assem.append(op);
|
||||||
assem.endDocument();
|
assem.endDocument();
|
||||||
|
|
||||||
// although we have instantiated the dstPad with '\n', an additional '\n' is
|
// 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
|
// create a changeset that removes the previous text and add the newText with
|
||||||
// all atributes present on the source pad
|
// 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);
|
dstPad.appendRevision(changeset, authorId);
|
||||||
|
|
||||||
await hooks.aCallAll('padCopy', {
|
await hooks.aCallAll('padCopy', {
|
||||||
|
@ -706,7 +707,7 @@ class Pad {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.batch(100).buffer(99);
|
.batch(100).buffer(99);
|
||||||
let atext = Changeset.makeAText('\n');
|
let atext = makeAText('\n');
|
||||||
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
for await (const [r, changeset, authorId, timestamp, isKeyRev, keyAText] of revs) {
|
||||||
try {
|
try {
|
||||||
assert(authorId != null);
|
assert(authorId != null);
|
||||||
|
@ -717,10 +718,10 @@ class Pad {
|
||||||
assert(timestamp > 0);
|
assert(timestamp > 0);
|
||||||
assert(changeset != null);
|
assert(changeset != null);
|
||||||
assert.equal(typeof changeset, 'string');
|
assert.equal(typeof changeset, 'string');
|
||||||
Changeset.checkRep(changeset);
|
checkRep(changeset);
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
let text = atext.text;
|
let text = atext.text;
|
||||||
for (const op of Changeset.deserializeOps(unpacked.ops)) {
|
for (const op of deserializeOps(unpacked.ops)) {
|
||||||
if (['=', '-'].includes(op.opcode)) {
|
if (['=', '-'].includes(op.opcode)) {
|
||||||
assert(text.length >= op.chars);
|
assert(text.length >= op.chars);
|
||||||
const consumed = text.slice(0, 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());
|
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);
|
if (isKeyRev) assert.deepEqual(keyAText, atext);
|
||||||
} catch (err:any) {
|
} catch (err:any) {
|
||||||
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
err.message = `(pad ${this.id} revision ${r}) ${err.message}`;
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
const padManager = require('../db/PadManager');
|
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 ChatMessage from '../../static/js/ChatMessage';
|
||||||
import AttributePool from '../../static/js/AttributePool';
|
import AttributePool from '../../static/js/AttributePool';
|
||||||
const AttributeManager = require('../../static/js/AttributeManager');
|
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 {APool, AText, PadAuthor, PadType} from "../types/PadType";
|
||||||
import {ChangeSet} from "../types/ChangeSet";
|
import {ChangeSet} from "../types/ChangeSet";
|
||||||
import {ChatMessageMessage, ClientReadyMessage, ClientSaveRevisionMessage, ClientSuggestUserName, ClientUserChangesMessage, ClientVarMessage, CustomMessage, UserNewInfoMessage} from "../../static/js/types/SocketIOMessage";
|
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 webaccess = require('../hooks/express/webaccess');
|
||||||
const { checkValidRev } = require('../utils/checkValidRev');
|
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);
|
const pad = await padManager.getPad(thisSession.padId, null, thisSession.author);
|
||||||
|
|
||||||
// Verify that the changeset has valid syntax and is in canonical form
|
// 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
|
// 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 add text with attribs
|
||||||
// = can change or add attribs
|
// = can change or add attribs
|
||||||
// - can have attribs, but they are discarded and don't show up in the 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
|
// ex. adoptChangesetAttribs
|
||||||
|
|
||||||
// Afaik, it copies the new attributes from the changeset, to the global Attribute Pool
|
// 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
|
// ex. applyUserChanges
|
||||||
let r = baseRev;
|
let r = baseRev;
|
||||||
|
@ -629,21 +630,21 @@ const handleUserChanges = async (socket:any, message: {
|
||||||
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
|
const {changeset: c, meta: {author: authorId}} = await pad.getRevision(r);
|
||||||
if (changeset === c && thisSession.author === authorId) {
|
if (changeset === c && thisSession.author === authorId) {
|
||||||
// Assume this is a retransmission of an already applied changeset.
|
// 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
|
// At this point, both "c" (from the pad) and "changeset" (from the
|
||||||
// client) are relative to revision r - 1. The follow function
|
// client) are relative to revision r - 1. The follow function
|
||||||
// rebases "changeset" so that it is relative to revision r
|
// rebases "changeset" so that it is relative to revision r
|
||||||
// and can be applied after "c".
|
// 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();
|
const prevText = pad.text();
|
||||||
|
|
||||||
if (Changeset.oldLen(rebasedChangeset) !== prevText.length) {
|
if (oldLen(rebasedChangeset) !== prevText.length) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can't apply changeset ${rebasedChangeset} with oldLen ` +
|
`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);
|
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.
|
// Make sure the pad always ends with an empty line.
|
||||||
if (pad.text().lastIndexOf('\n') !== pad.text().length - 1) {
|
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);
|
await pad.appendRevision(nlChangeset, thisSession.author);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,7 +714,7 @@ exports.updatePadClients = async (pad: PadType) => {
|
||||||
const revChangeset = revision.changeset;
|
const revChangeset = revision.changeset;
|
||||||
const currentTime = revision.meta.timestamp;
|
const currentTime = revision.meta.timestamp;
|
||||||
|
|
||||||
const forWire = Changeset.prepareForWire(revChangeset, pad.pool);
|
const forWire = prepareForWire(revChangeset, pad.pool);
|
||||||
const msg = {
|
const msg = {
|
||||||
type: 'COLLABROOM',
|
type: 'COLLABROOM',
|
||||||
data: {
|
data: {
|
||||||
|
@ -748,7 +749,7 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||||
// that aren't at the start of a line
|
// that aren't at the start of a line
|
||||||
const badMarkers = [];
|
const badMarkers = [];
|
||||||
let offset = 0;
|
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 attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
const hasMarker = AttributeManager.lineAttributes.some((a: string) => attribs.has(a));
|
||||||
if (hasMarker) {
|
if (hasMarker) {
|
||||||
|
@ -770,7 +771,7 @@ const _correctMarkersInPad = (atext: AText, apool: AttributePool) => {
|
||||||
// create changeset that removes these bad markers
|
// create changeset that removes these bad markers
|
||||||
offset = 0;
|
offset = 0;
|
||||||
|
|
||||||
const builder = Changeset.builder(text.length);
|
const builder = new Builder(text.length);
|
||||||
|
|
||||||
badMarkers.forEach((pos) => {
|
badMarkers.forEach((pos) => {
|
||||||
builder.keepText(text.substring(offset, pos));
|
builder.keepText(text.substring(offset, pos));
|
||||||
|
@ -905,7 +906,7 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
||||||
|
|
||||||
// return pending changesets
|
// return pending changesets
|
||||||
for (const r of revisionsNeeded) {
|
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',
|
const wireMsg = {type: 'COLLABROOM',
|
||||||
data: {type: 'CLIENT_RECONNECT',
|
data: {type: 'CLIENT_RECONNECT',
|
||||||
headRev: pad.getHeadRevisionNumber(),
|
headRev: pad.getHeadRevisionNumber(),
|
||||||
|
@ -930,8 +931,8 @@ const handleClientReady = async (socket:any, message: ClientReadyMessage) => {
|
||||||
let apool;
|
let apool;
|
||||||
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
// prepare all values for the wire, there's a chance that this throws, if the pad is corrupted
|
||||||
try {
|
try {
|
||||||
atext = Changeset.cloneAText(pad.atext);
|
atext = cloneAText(pad.atext);
|
||||||
const attribsForWire = Changeset.prepareForWire(atext.attribs, pad.pool);
|
const attribsForWire = prepareForWire(atext.attribs, pad.pool);
|
||||||
apool = attribsForWire.pool.toJsonable();
|
apool = attribsForWire.pool.toJsonable();
|
||||||
atext.attribs = attribsForWire.translated;
|
atext.attribs = attribsForWire.translated;
|
||||||
} catch (e:any) {
|
} catch (e:any) {
|
||||||
|
@ -1167,13 +1168,13 @@ const getChangesetInfo = async (pad: PadType, startNum: number, endNum:number, g
|
||||||
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
if (compositeEnd > endNum || compositeEnd > headRevision + 1) break;
|
||||||
|
|
||||||
const forwards = composedChangesets[`${compositeStart}/${compositeEnd}`];
|
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());
|
mutateAttributionLines(forwards, lines.alines, pad.apool());
|
||||||
Changeset.mutateTextLines(forwards, lines.textlines);
|
mutateTextLines(forwards, lines.textlines);
|
||||||
|
|
||||||
const forwards2 = Changeset.moveOpsToNewPool(forwards, pad.apool(), apool);
|
const forwards2 = moveOpsToNewPool(forwards, pad.apool(), apool);
|
||||||
const backwards2 = Changeset.moveOpsToNewPool(backwards, pad.apool(), apool);
|
const backwards2 = moveOpsToNewPool(backwards, pad.apool(), apool);
|
||||||
|
|
||||||
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
const t1 = (compositeStart === 0) ? revisionDate[0] : revisionDate[compositeStart - 1];
|
||||||
const t2 = revisionDate[compositeEnd - 1];
|
const t2 = revisionDate[compositeEnd - 1];
|
||||||
|
@ -1199,12 +1200,12 @@ const getPadLines = async (pad: PadType, revNum: number) => {
|
||||||
if (revNum >= 0) {
|
if (revNum >= 0) {
|
||||||
atext = await pad.getInternalRevisionAText(revNum);
|
atext = await pad.getInternalRevisionAText(revNum);
|
||||||
} else {
|
} else {
|
||||||
atext = Changeset.makeAText('\n');
|
atext = makeAText('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
textlines: Changeset.splitTextLines(atext.text),
|
textlines: splitTextLines(atext.text),
|
||||||
alines: Changeset.splitAttributionLines(atext.attribs, 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++) {
|
for (r = startNum + 1; r < endNum; r++) {
|
||||||
const cs = changesets[r];
|
const cs = changesets[r];
|
||||||
changeset = Changeset.compose(changeset, cs, pool);
|
changeset = compose(changeset as string, cs as string, pool);
|
||||||
}
|
}
|
||||||
return changeset;
|
return changeset;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {MapArrayType} from "./MapType";
|
import {MapArrayType} from "./MapType";
|
||||||
|
import AttributePool from "../../static/js/AttributePool";
|
||||||
|
|
||||||
export type PadType = {
|
export type PadType = {
|
||||||
id: string,
|
id: string,
|
||||||
apool: ()=>APool,
|
apool: ()=>AttributePool,
|
||||||
atext: AText,
|
atext: AText,
|
||||||
pool: APool,
|
pool: AttributePool,
|
||||||
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
|
getInternalRevisionAText: (text:number|string)=>Promise<AText>,
|
||||||
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
|
getValidRevisionRange: (fromRev: string, toRev: string)=>PadRange,
|
||||||
getRevisionAuthor: (rev: number)=>Promise<string>,
|
getRevisionAuthor: (rev: number)=>Promise<string>,
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
import AttributeMap from '../../static/js/AttributeMap';
|
||||||
import AttributePool from "../../static/js/AttributePool";
|
import AttributePool from "../../static/js/AttributePool";
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps, splitAttributionLines, subattribution} from '../../static/js/Changeset';
|
||||||
const { checkValidRev } = require('./checkValidRev');
|
const { checkValidRev } = require('./checkValidRev');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -31,7 +31,7 @@ exports.getPadPlainText = (pad: { getInternalRevisionAText: (arg0: any) => any;
|
||||||
const _analyzeLine = exports._analyzeLine;
|
const _analyzeLine = exports._analyzeLine;
|
||||||
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
const atext = ((revNum !== undefined) ? pad.getInternalRevisionAText(checkValidRev(revNum)) : pad.atext);
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
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 apool = pad.pool;
|
||||||
|
|
||||||
const pieces = [];
|
const pieces = [];
|
||||||
|
@ -52,14 +52,14 @@ type LineModel = {
|
||||||
[id:string]:string|number|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 = {};
|
const line: LineModel = {};
|
||||||
|
|
||||||
// identify list
|
// identify list
|
||||||
let lineMarker = 0;
|
let lineMarker = 0;
|
||||||
line.listLevel = 0;
|
line.listLevel = 0;
|
||||||
if (aline) {
|
if (aline) {
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op != null) {
|
if (op != null) {
|
||||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
let listType = attribs.get('list');
|
let listType = attribs.get('list');
|
||||||
|
@ -79,7 +79,7 @@ exports._analyzeLine = (text:string, aline: LineModel, apool: AttributePool) =>
|
||||||
}
|
}
|
||||||
if (lineMarker) {
|
if (lineMarker) {
|
||||||
line.text = text.substring(1);
|
line.text = text.substring(1);
|
||||||
line.aline = Changeset.subattribution(aline, 1);
|
line.aline = subattribution(aline, 1);
|
||||||
} else {
|
} else {
|
||||||
line.text = text;
|
line.text = text;
|
||||||
line.aline = aline;
|
line.aline = aline;
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {MapArrayType} from "../types/MapType";
|
||||||
* limitations under the License.
|
* 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 attributes = require('../../static/js/attributes');
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require('../db/PadManager');
|
||||||
const _ = require('underscore');
|
const _ = require('underscore');
|
||||||
|
@ -28,6 +28,8 @@ const eejs = require('../eejs');
|
||||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
||||||
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
const _encodeWhitespace = require('./ExportHelper')._encodeWhitespace;
|
||||||
import padutils from "../../static/js/pad_utils";
|
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) => {
|
const getPadHTML = async (pad: PadType, revNum: string) => {
|
||||||
let atext = pad.atext;
|
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 getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string[]) => {
|
||||||
const apool = pad.apool();
|
const apool = pad.apool();
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
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 tags = ['h1', 'h2', 'strong', 'em', 'u', 's'];
|
||||||
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
const props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||||
|
@ -80,6 +82,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
css += '<style>\n';
|
css += '<style>\n';
|
||||||
|
|
||||||
for (const a of Object.keys(apool.numToAttrib)) {
|
for (const a of Object.keys(apool.numToAttrib)) {
|
||||||
|
// @ts-ignore
|
||||||
const attr = apool.numToAttrib[a];
|
const attr = apool.numToAttrib[a];
|
||||||
|
|
||||||
// skip non author attributes
|
// skip non author attributes
|
||||||
|
@ -115,6 +118,7 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
// see hook exportHtmlAdditionalTagsWithData
|
// see hook exportHtmlAdditionalTagsWithData
|
||||||
attrib = propName;
|
attrib = propName;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
const propTrueNum = apool.putAttrib(attrib, true);
|
const propTrueNum = apool.putAttrib(attrib, true);
|
||||||
if (propTrueNum >= 0) {
|
if (propTrueNum >= 0) {
|
||||||
anumMap[propTrueNum] = i;
|
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>
|
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
// becomes
|
// becomes
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
const taker = Changeset.stringIterator(text);
|
const taker = new StringIterator(text);
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
const openTags:string[] = [];
|
const openTags:string[] = [];
|
||||||
|
|
||||||
const getSpanClassFor = (i: string) => {
|
const getSpanClassFor = (i: string) => {
|
||||||
|
@ -204,7 +208,8 @@ const getHTMLFromAtext = async (pad:PadType, atext: AText, authorColors?: string
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
// @ts-ignore
|
||||||
|
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||||
idx += numChars;
|
idx += numChars;
|
||||||
|
|
||||||
// this iterates over every op string and decides which tags to open or to close
|
// 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 {AText, PadType} from "../types/PadType";
|
||||||
import {MapType} from "../types/MapType";
|
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 attributes = require('../../static/js/attributes');
|
||||||
const padManager = require('../db/PadManager');
|
const padManager = require('../db/PadManager');
|
||||||
const _analyzeLine = require('./ExportHelper')._analyzeLine;
|
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 getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
const apool = pad.apool();
|
const apool = pad.apool();
|
||||||
const textLines = atext.text.slice(0, -1).split('\n');
|
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 props = ['heading1', 'heading2', 'bold', 'italic', 'underline', 'strikethrough'];
|
||||||
const anumMap: MapType = {};
|
const anumMap: MapType = {};
|
||||||
const css = '';
|
const css = '';
|
||||||
|
|
||||||
props.forEach((propName, i) => {
|
props.forEach((propName, i) => {
|
||||||
|
// @ts-ignore
|
||||||
const propTrueNum = apool.putAttrib([propName, true], true);
|
const propTrueNum = apool.putAttrib([propName, true], true);
|
||||||
if (propTrueNum >= 0) {
|
if (propTrueNum >= 0) {
|
||||||
anumMap[propTrueNum] = i;
|
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>
|
// <b>Just bold<b> <b><i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
// becomes
|
// becomes
|
||||||
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
// <b>Just bold <i>Bold and italics</i></b> <i>Just italics</i>
|
||||||
const taker = Changeset.stringIterator(text);
|
const taker = new StringIterator(text);
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
|
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
|
@ -79,7 +82,7 @@ const getTXTFromAtext = (pad: PadType, atext: AText, authorColors?:string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ops = Changeset.deserializeOps(Changeset.subattribution(attribs, idx, idx + numChars));
|
const ops = deserializeOps(subattribution(attribs, idx, idx + numChars));
|
||||||
idx += numChars;
|
idx += numChars;
|
||||||
|
|
||||||
for (const o of ops) {
|
for (const o of ops) {
|
||||||
|
|
|
@ -16,10 +16,11 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import log4js from 'log4js';
|
import log4js from 'log4js';
|
||||||
const Changeset = require('../../static/js/Changeset');
|
import {deserializeOps} from '../../static/js/Changeset';
|
||||||
const contentcollector = require('../../static/js/contentcollector');
|
const contentcollector = require('../../static/js/contentcollector');
|
||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
import {PadType} from "../types/PadType";
|
import {PadType} from "../types/PadType";
|
||||||
|
import {Builder} from "../../static/js/Builder";
|
||||||
|
|
||||||
const apiLogger = log4js.getLogger('ImportHtml');
|
const apiLogger = log4js.getLogger('ImportHtml');
|
||||||
let processor:any;
|
let processor:any;
|
||||||
|
@ -69,13 +70,13 @@ exports.setPadHTML = async (pad: PadType, html:string, authorId = '') => {
|
||||||
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
const newAttribs = `${result.lineAttribs.join('|1+1')}|1+1`;
|
||||||
|
|
||||||
// create a new changeset with a helper builder object
|
// 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
|
// assemble each line into the builder
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = 0;
|
const newTextStart = 0;
|
||||||
const newTextEnd = newText.length;
|
const newTextEnd = newText.length;
|
||||||
for (const op of Changeset.deserializeOps(newAttribs)) {
|
for (const op of deserializeOps(newAttribs)) {
|
||||||
const nextIndex = textIndex + op.chars;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
const start = Math.max(newTextStart, textIndex);
|
const start = Math.max(newTextStart, textIndex);
|
||||||
|
|
|
@ -4,7 +4,12 @@ import {PadAuthor, PadType} from "../types/PadType";
|
||||||
import {MapArrayType} from "../types/MapType";
|
import {MapArrayType} from "../types/MapType";
|
||||||
|
|
||||||
import AttributeMap from '../../static/js/AttributeMap';
|
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 attributes = require('../../static/js/attributes');
|
||||||
const exportHtml = require('./ExportHtml');
|
const exportHtml = require('./ExportHtml');
|
||||||
|
|
||||||
|
@ -33,7 +38,7 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
_isClearAuthorship(changeset: any){
|
_isClearAuthorship(changeset: any){
|
||||||
// unpack
|
// unpack
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
|
|
||||||
// check if there is nothing in the charBank
|
// check if there is nothing in the charBank
|
||||||
if (unpacked.charBank !== '') {
|
if (unpacked.charBank !== '') {
|
||||||
|
@ -45,7 +50,7 @@ class PadDiff {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [clearOperator, anotherOp] = Changeset.deserializeOps(unpacked.ops);
|
const [clearOperator, anotherOp] = deserializeOps(unpacked.ops);
|
||||||
|
|
||||||
// check if there is only one operator
|
// check if there is only one operator
|
||||||
if (anotherOp != null) return false;
|
if (anotherOp != null) return false;
|
||||||
|
@ -78,7 +83,7 @@ class PadDiff {
|
||||||
const atext = await this._pad.getInternalRevisionAText(rev);
|
const atext = await this._pad.getInternalRevisionAText(rev);
|
||||||
|
|
||||||
// build clearAuthorship changeset
|
// 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);
|
builder.keepText(atext.text, [['author', '']], this._pad.pool);
|
||||||
const changeset = builder.toString();
|
const changeset = builder.toString();
|
||||||
|
|
||||||
|
@ -93,7 +98,7 @@ class PadDiff {
|
||||||
const changeset = await this._createClearAuthorship(rev);
|
const changeset = await this._createClearAuthorship(rev);
|
||||||
|
|
||||||
// apply the clearAuthorship changeset
|
// apply the clearAuthorship changeset
|
||||||
const newAText = Changeset.applyToAText(changeset, atext, this._pad.pool);
|
const newAText = applyToAText(changeset, atext, this._pad.pool);
|
||||||
|
|
||||||
return newAText;
|
return newAText;
|
||||||
}
|
}
|
||||||
|
@ -157,7 +162,7 @@ class PadDiff {
|
||||||
if (superChangeset == null) {
|
if (superChangeset == null) {
|
||||||
superChangeset = changeset;
|
superChangeset = changeset;
|
||||||
} else {
|
} 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);
|
const deletionChangeset = this._createDeletionChangeset(superChangeset, atext, this._pad.pool);
|
||||||
|
|
||||||
// apply the superChangeset, which includes all addings
|
// 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
|
// apply the deletionChangeset, which adds a deletions
|
||||||
atext = Changeset.applyToAText(deletionChangeset, atext, this._pad.pool);
|
atext = applyToAText(deletionChangeset, atext, this._pad.pool);
|
||||||
}
|
}
|
||||||
|
|
||||||
return atext;
|
return atext;
|
||||||
|
@ -209,22 +214,22 @@ class PadDiff {
|
||||||
|
|
||||||
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
_extendChangesetWithAuthor(changeset: any, author: any, apool: any){
|
||||||
// unpack
|
// unpack
|
||||||
const unpacked = Changeset.unpack(changeset);
|
const unpacked = unpack(changeset);
|
||||||
|
|
||||||
const assem = Changeset.opAssembler();
|
const assem = new OpAssembler();
|
||||||
|
|
||||||
// create deleted attribs
|
// create deleted attribs
|
||||||
const authorAttrib = apool.putAttrib(['author', author || '']);
|
const authorAttrib = apool.putAttrib(['author', author || '']);
|
||||||
const deletedAttrib = apool.putAttrib(['removed', true]);
|
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 === '-') {
|
if (operator.opcode === '-') {
|
||||||
// this is a delete operator, extend it with the author
|
// this is a delete operator, extend it with the author
|
||||||
operator.attribs = attribs;
|
operator.attribs = attribs;
|
||||||
} else if (operator.opcode === '=' && operator.attribs) {
|
} else if (operator.opcode === '=' && operator.attribs) {
|
||||||
// this is operator changes only attributes, let's mark which author did that
|
// 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
|
// append the new operator to our assembler
|
||||||
|
@ -232,26 +237,31 @@ class PadDiff {
|
||||||
}
|
}
|
||||||
|
|
||||||
// return the modified changeset
|
// 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){
|
_createDeletionChangeset(cs: any, startAText: any, apool: any){
|
||||||
const lines = Changeset.splitTextLines(startAText.text);
|
const lines = splitTextLines(startAText.text);
|
||||||
const alines = Changeset.splitAttributionLines(startAText.attribs, startAText.text);
|
const alines = splitAttributionLines(startAText.attribs, startAText.text);
|
||||||
|
|
||||||
// lines and alines are what the exports is meant to apply to.
|
// 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 may be arrays or objects with .get(i) and .length methods.
|
||||||
// They include final newlines on lines.
|
// They include final newlines on lines.
|
||||||
|
|
||||||
const linesGet = (idx: number) => {
|
const linesGet = (idx: number) => {
|
||||||
|
// @ts-ignore
|
||||||
if (lines.get) {
|
if (lines.get) {
|
||||||
|
// @ts-ignore
|
||||||
return lines.get(idx);
|
return lines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
return lines[idx];
|
return lines[idx];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const aLinesGet = (idx: number) => {
|
const aLinesGet = (idx: number) => {
|
||||||
|
// @ts-ignore
|
||||||
if (alines.get) {
|
if (alines.get) {
|
||||||
|
// @ts-ignore
|
||||||
return alines.get(idx);
|
return alines.get(idx);
|
||||||
} else {
|
} else {
|
||||||
return alines[idx];
|
return alines[idx];
|
||||||
|
@ -263,14 +273,14 @@ class PadDiff {
|
||||||
let curLineOps: { next: () => any; } | null = null;
|
let curLineOps: { next: () => any; } | null = null;
|
||||||
let curLineOpsNext: { done: any; value: any; } | null = null;
|
let curLineOpsNext: { done: any; value: any; } | null = null;
|
||||||
let curLineOpsLine: number;
|
let curLineOpsLine: number;
|
||||||
let curLineNextOp = new Changeset.Op('+');
|
let curLineNextOp = new Op('+');
|
||||||
|
|
||||||
const unpacked = Changeset.unpack(cs);
|
const unpacked = unpack(cs);
|
||||||
const builder = Changeset.builder(unpacked.newLen);
|
const builder = new Builder(unpacked.newLen);
|
||||||
|
|
||||||
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
const consumeAttribRuns = (numChars: number, func: Function /* (len, attribs, endsLine)*/) => {
|
||||||
if (!curLineOps || curLineOpsLine !== curLine) {
|
if (!curLineOps || curLineOpsLine !== curLine) {
|
||||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
curLineOpsLine = curLine;
|
curLineOpsLine = curLine;
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
|
@ -291,13 +301,13 @@ class PadDiff {
|
||||||
curChar = 0;
|
curChar = 0;
|
||||||
curLineOpsLine = curLine;
|
curLineOpsLine = curLine;
|
||||||
curLineNextOp.chars = 0;
|
curLineNextOp.chars = 0;
|
||||||
curLineOps = Changeset.deserializeOps(aLinesGet(curLine));
|
curLineOps = deserializeOps(aLinesGet(curLine));
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!curLineNextOp.chars) {
|
if (!curLineNextOp.chars) {
|
||||||
if (curLineOpsNext!.done) {
|
if (curLineOpsNext!.done) {
|
||||||
curLineNextOp = new Changeset.Op();
|
curLineNextOp = new Op();
|
||||||
} else {
|
} else {
|
||||||
curLineNextOp = curLineOpsNext!.value;
|
curLineNextOp = curLineOpsNext!.value;
|
||||||
curLineOpsNext = curLineOps!.next();
|
curLineOpsNext = curLineOps!.next();
|
||||||
|
@ -332,7 +342,7 @@ class PadDiff {
|
||||||
|
|
||||||
const nextText = (numChars: number) => {
|
const nextText = (numChars: number) => {
|
||||||
let len = 0;
|
let len = 0;
|
||||||
const assem = Changeset.stringAssembler();
|
const assem = new StringAssembler();
|
||||||
const firstString = linesGet(curLine).substring(curChar);
|
const firstString = linesGet(curLine).substring(curChar);
|
||||||
len += firstString.length;
|
len += firstString.length;
|
||||||
assem.append(firstString);
|
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 === '=') {
|
if (csOp.opcode === '=') {
|
||||||
const textBank = nextText(csOp.chars);
|
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,
|
// this method is 80% like Changeset.inverse. I just changed so instead of reverting,
|
||||||
// it adds deletions and attribute changes to the atext.
|
// it adds deletions and attribute changes to the atext.
|
||||||
|
// @ts-ignore
|
||||||
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
PadDiff.prototype._createDeletionChangeset = function (cs, startAText, apool) {
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
'use strict';
|
|
||||||
|
|
||||||
import AttributeMap from './AttributeMap';
|
import AttributeMap from './AttributeMap';
|
||||||
const Changeset = require('./Changeset');
|
import {compose, deserializeOps, isIdentity} from './Changeset';
|
||||||
const ChangesetUtils = require('./ChangesetUtils');
|
import {Builder} from "./Builder";
|
||||||
const attributes = require('./attributes');
|
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils';
|
||||||
const underscore = require("underscore")
|
import attributes from './attributes';
|
||||||
|
import underscore from "underscore";
|
||||||
|
|
||||||
const lineMarkerAttribute = 'lmkr';
|
const lineMarkerAttribute = 'lmkr';
|
||||||
|
|
||||||
|
@ -52,7 +51,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
if (!this.applyChangesetCallback) return changeset;
|
if (!this.applyChangesetCallback) return changeset;
|
||||||
|
|
||||||
const cs = changeset.toString();
|
const cs = changeset.toString();
|
||||||
if (!Changeset.isIdentity(cs)) {
|
if (!isIdentity(cs)) {
|
||||||
this.applyChangesetCallback(cs);
|
this.applyChangesetCallback(cs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +85,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
// as the range might not be continuous
|
// as the range might not be continuous
|
||||||
// due to the presence of line markers on the rows
|
// due to the presence of line markers on the rows
|
||||||
if (allChangesets) {
|
if (allChangesets) {
|
||||||
allChangesets = Changeset.compose(
|
allChangesets = compose(
|
||||||
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
allChangesets.toString(), rowChangeset.toString(), this.rep.apool);
|
||||||
} else {
|
} else {
|
||||||
allChangesets = rowChangeset;
|
allChangesets = rowChangeset;
|
||||||
|
@ -126,9 +125,9 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
* @param attribs an array of attributes
|
* @param attribs an array of attributes
|
||||||
*/
|
*/
|
||||||
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
_setAttributesOnRangeByLine(row, startCol, endCol, attribs) {
|
||||||
const builder = Changeset.builder(this.rep.lines.totalWidth());
|
const builder = new Builder(this.rep.lines.totalWidth());
|
||||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
buildKeepToStartOfRange(this.rep, builder, [row, startCol]);
|
||||||
ChangesetUtils.buildKeepRange(
|
buildKeepRange(
|
||||||
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
this.rep, builder, [row, startCol], [row, endCol], attribs, this.rep.apool);
|
||||||
return builder;
|
return builder;
|
||||||
},
|
},
|
||||||
|
@ -151,7 +150,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
// get `attributeName` attribute of first char of line
|
// get `attributeName` attribute of first char of line
|
||||||
const aline = this.rep.alines[lineNum];
|
const aline = this.rep.alines[lineNum];
|
||||||
if (!aline) return '';
|
if (!aline) return '';
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op == null) return '';
|
if (op == null) return '';
|
||||||
return AttributeMap.fromString(op.attribs, this.rep.apool).get(attributeName) || '';
|
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
|
// get attributes of first char of line
|
||||||
const aline = this.rep.alines[lineNum];
|
const aline = this.rep.alines[lineNum];
|
||||||
if (!aline) return [];
|
if (!aline) return [];
|
||||||
const [op] = Changeset.deserializeOps(aline);
|
const [op] = deserializeOps(aline);
|
||||||
if (op == null) return [];
|
if (op == null) return [];
|
||||||
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
return [...attributes.attribsFromString(op.attribs, this.rep.apool)];
|
||||||
},
|
},
|
||||||
|
@ -222,7 +221,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
let hasAttrib = true;
|
let hasAttrib = true;
|
||||||
|
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||||
const opStartInLine = indexIntoLine;
|
const opStartInLine = indexIntoLine;
|
||||||
const opEndInLine = opStartInLine + op.chars;
|
const opEndInLine = opStartInLine + op.chars;
|
||||||
if (!hasIt(op.attribs)) {
|
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
|
// we need to sum up how much characters each operations take until the wanted position
|
||||||
let currentPointer = 0;
|
let currentPointer = 0;
|
||||||
|
|
||||||
for (const currentOperation of Changeset.deserializeOps(aline)) {
|
for (const currentOperation of deserializeOps(aline)) {
|
||||||
currentPointer += currentOperation.chars;
|
currentPointer += currentOperation.chars;
|
||||||
if (currentPointer <= column) continue;
|
if (currentPointer <= column) continue;
|
||||||
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
return [...attributes.attribsFromString(currentOperation.attribs, this.rep.apool)];
|
||||||
|
@ -286,13 +285,13 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
*/
|
*/
|
||||||
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
setAttributeOnLine(lineNum, attributeName, attributeValue) {
|
||||||
let loc = [0, 0];
|
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);
|
const hasMarker = this.lineHasMarker(lineNum);
|
||||||
|
|
||||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 0]));
|
||||||
|
|
||||||
if (hasMarker) {
|
if (hasMarker) {
|
||||||
ChangesetUtils.buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
buildKeepRange(this.rep, builder, loc, (loc = [lineNum, 1]), [
|
||||||
[attributeName, attributeValue],
|
[attributeName, attributeValue],
|
||||||
], this.rep.apool);
|
], this.rep.apool);
|
||||||
} else {
|
} else {
|
||||||
|
@ -315,7 +314,7 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
* @param attributeValue if given only attributes with equal value will be removed
|
* @param attributeValue if given only attributes with equal value will be removed
|
||||||
*/
|
*/
|
||||||
removeAttributeOnLine(lineNum, attributeName, attributeValue) {
|
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);
|
const hasMarker = this.lineHasMarker(lineNum);
|
||||||
let found = false;
|
let found = false;
|
||||||
|
|
||||||
|
@ -334,16 +333,16 @@ AttributeManager.prototype = underscore.default(AttributeManager.prototype).exte
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangesetUtils.buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
buildKeepToStartOfRange(this.rep, builder, [lineNum, 0]);
|
||||||
|
|
||||||
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
const countAttribsWithMarker = underscore.chain(attribs).filter((a) => !!a[1])
|
||||||
.map((a) => a[0]).difference(DEFAULT_LINE_ATTRIBUTES).size().value();
|
.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 we have marker and any of attributes don't need to have marker. we need delete it
|
||||||
if (hasMarker && !countAttribsWithMarker) {
|
if (hasMarker && !countAttribsWithMarker) {
|
||||||
ChangesetUtils.buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
buildRemoveRange(this.rep, builder, [lineNum, 0], [lineNum, 1]);
|
||||||
} else {
|
} else {
|
||||||
ChangesetUtils.buildKeepRange(
|
buildKeepRange(
|
||||||
this.rep, builder, [lineNum, 0], [lineNum, 1], attribs, this.rep.apool);
|
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';
|
'use strict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,6 +5,12 @@
|
||||||
* based on a SkipList
|
* 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.
|
* Copyright 2009 Google Inc.
|
||||||
*
|
*
|
||||||
|
@ -21,7 +26,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* 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 startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||||
const endLineOffset = rep.lines.offsetOfIndex(end[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 startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||||
const endLineOffset = rep.lines.offsetOfIndex(end[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]);
|
const startLineOffset = rep.lines.offsetOfIndex(start[0]);
|
||||||
|
|
||||||
builder.keep(startLineOffset, start[0]);
|
builder.keep(startLineOffset, start[0]);
|
||||||
builder.keep(start[1]);
|
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
|
// @ts-nocheck
|
||||||
'use strict';
|
import {Builder} from "./Builder";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
|
@ -24,6 +24,8 @@ const browser = require('./vendors/browser');
|
||||||
import padutils from './pad_utils'
|
import padutils from './pad_utils'
|
||||||
const Ace2Common = require('./ace2_common');
|
const Ace2Common = require('./ace2_common');
|
||||||
const $ = require('./rjquery').$;
|
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 isNodeText = Ace2Common.isNodeText;
|
||||||
const getAssoc = Ace2Common.getAssoc;
|
const getAssoc = Ace2Common.getAssoc;
|
||||||
|
@ -33,14 +35,15 @@ const hooks = require('./pluginfw/hooks');
|
||||||
import SkipList from "./skiplist";
|
import SkipList from "./skiplist";
|
||||||
import Scroll from './scroll'
|
import Scroll from './scroll'
|
||||||
import AttribPool from './AttributePool'
|
import AttribPool from './AttributePool'
|
||||||
|
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||||
|
import Op from "./Op";
|
||||||
|
import {buildKeepRange, buildKeepToStartOfRange, buildRemoveRange} from './ChangesetUtils'
|
||||||
|
|
||||||
function Ace2Inner(editorInfo, cssManagers) {
|
function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
const makeChangesetTracker = require('./changesettracker').makeChangesetTracker;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
const makeContentCollector = require('./contentcollector').makeContentCollector;
|
||||||
const domline = require('./domline').domline;
|
const domline = require('./domline').domline;
|
||||||
const Changeset = require('./Changeset');
|
|
||||||
const ChangesetUtils = require('./ChangesetUtils');
|
|
||||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||||
const undoModule = require('./undomodule').undoModule;
|
const undoModule = require('./undomodule').undoModule;
|
||||||
const AttributeManager = require('./AttributeManager');
|
const AttributeManager = require('./AttributeManager');
|
||||||
|
@ -174,9 +177,9 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// CCCCCCCCCCCCCCCCCCCC\n
|
// CCCCCCCCCCCCCCCCCCCC\n
|
||||||
// CCCC\n
|
// CCCC\n
|
||||||
// end[0]: <CCC end[1] CCC>-------\n
|
// end[0]: <CCC end[1] CCC>-------\n
|
||||||
const builder = Changeset.builder(rep.lines.totalWidth());
|
const builder = new Builder(rep.lines.totalWidth());
|
||||||
ChangesetUtils.buildKeepToStartOfRange(rep, builder, start);
|
buildKeepToStartOfRange(rep, builder, start);
|
||||||
ChangesetUtils.buildRemoveRange(rep, builder, start, end);
|
buildRemoveRange(rep, builder, start, end);
|
||||||
builder.insert(newText, [
|
builder.insert(newText, [
|
||||||
['author', thisAuthor],
|
['author', thisAuthor],
|
||||||
], rep.apool);
|
], rep.apool);
|
||||||
|
@ -495,10 +498,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const importAText = (atext, apoolJsonObj, undoable) => {
|
const importAText = (atext, apoolJsonObj, undoable) => {
|
||||||
atext = Changeset.cloneAText(atext);
|
atext = cloneAText(atext);
|
||||||
if (apoolJsonObj) {
|
if (apoolJsonObj) {
|
||||||
const wireApool = (new AttribPool()).fromJsonable(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' : ''}`, () => {
|
inCallStackIfNecessary(`importText${undoable ? 'Undoable' : ''}`, () => {
|
||||||
setDocAText(atext);
|
setDocAText(atext);
|
||||||
|
@ -527,18 +530,18 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const numLines = rep.lines.length();
|
const numLines = rep.lines.length();
|
||||||
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
const upToLastLine = rep.lines.offsetOfIndex(numLines - 1);
|
||||||
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
const lastLineLength = rep.lines.atIndex(numLines - 1).text.length;
|
||||||
const assem = Changeset.smartOpAssembler();
|
const assem = new SmartOpAssembler();
|
||||||
const o = new Changeset.Op('-');
|
const o = new Op('-');
|
||||||
o.chars = upToLastLine;
|
o.chars = upToLastLine;
|
||||||
o.lines = numLines - 1;
|
o.lines = numLines - 1;
|
||||||
assem.append(o);
|
assem.append(o);
|
||||||
o.chars = lastLineLength;
|
o.chars = lastLineLength;
|
||||||
o.lines = 0;
|
o.lines = 0;
|
||||||
assem.append(o);
|
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 newLen = oldLen + assem.getLengthChange();
|
||||||
const changeset = Changeset.checkRep(
|
const changeset = checkRep(
|
||||||
Changeset.pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
pack(oldLen, newLen, assem.toString(), atext.text.slice(0, -1)));
|
||||||
performDocumentApplyChangeset(changeset);
|
performDocumentApplyChangeset(changeset);
|
||||||
|
|
||||||
performSelectionChange(
|
performSelectionChange(
|
||||||
|
@ -552,7 +555,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const setDocText = (text) => {
|
const setDocText = (text) => {
|
||||||
setDocAText(Changeset.makeAText(text));
|
setDocAText(makeAText(text));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDocText = () => {
|
const getDocText = () => {
|
||||||
|
@ -1271,7 +1274,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
if (shouldIndent && /[[(:{]\s*$/.exec(prevLineText)) {
|
||||||
theIndent += THE_TAB;
|
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(
|
rep.lines.offsetOfIndex(lineNum), lineNum).insert(
|
||||||
theIndent, [
|
theIndent, [
|
||||||
['author', thisAuthor],
|
['author', thisAuthor],
|
||||||
|
@ -1423,7 +1426,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
|
const selStartChar = rep.lines.offsetOfIndex(rep.selStart[0]) + rep.selStart[1];
|
||||||
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
|
const selEndChar = rep.lines.offsetOfIndex(rep.selEnd[0]) + rep.selEnd[1];
|
||||||
const result =
|
const result =
|
||||||
Changeset.characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
characterRangeFollow(changes, selStartChar, selEndChar, insertsAfterSelection);
|
||||||
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
|
requiredSelectionSetting = [result[0], result[1], rep.selFocusAtStart];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1435,7 +1438,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
length: () => rep.lines.length(),
|
length: () => rep.lines.length(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Changeset.mutateTextLines(changes, linesMutatee);
|
mutateTextLines(changes, linesMutatee);
|
||||||
|
|
||||||
if (requiredSelectionSetting) {
|
if (requiredSelectionSetting) {
|
||||||
performSelectionChange(
|
performSelectionChange(
|
||||||
|
@ -1446,10 +1449,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
|
const doRepApplyChangeset = (changes, insertsAfterSelection) => {
|
||||||
Changeset.checkRep(changes);
|
checkRep(changes);
|
||||||
|
|
||||||
if (Changeset.oldLen(changes) !== rep.alltext.length) {
|
if (oldLen(changes) !== rep.alltext.length) {
|
||||||
const errMsg = `${Changeset.oldLen(changes)}/${rep.alltext.length}`;
|
const errMsg = `${oldLen(changes)}/${rep.alltext.length}`;
|
||||||
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
|
throw new Error(`doRepApplyChangeset length mismatch: ${errMsg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1458,10 +1461,10 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (!editEvent.changeset) {
|
if (!editEvent.changeset) {
|
||||||
editEvent.changeset = changes;
|
editEvent.changeset = changes;
|
||||||
} else {
|
} else {
|
||||||
editEvent.changeset = Changeset.compose(editEvent.changeset, changes, rep.apool);
|
editEvent.changeset = compose(editEvent.changeset, changes, rep.apool);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const inverseChangeset = Changeset.inverse(changes, {
|
const inverseChangeset = inverse(changes, {
|
||||||
get: (i) => `${rep.lines.atIndex(i).text}\n`,
|
get: (i) => `${rep.lines.atIndex(i).text}\n`,
|
||||||
length: () => rep.lines.length(),
|
length: () => rep.lines.length(),
|
||||||
}, rep.alines, rep.apool);
|
}, rep.alines, rep.apool);
|
||||||
|
@ -1469,11 +1472,11 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (!editEvent.backset) {
|
if (!editEvent.backset) {
|
||||||
editEvent.backset = inverseChangeset;
|
editEvent.backset = inverseChangeset;
|
||||||
} else {
|
} 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()) {
|
if (changesetTracker.isTracking()) {
|
||||||
changesetTracker.composeUserChangeset(changes);
|
changesetTracker.composeUserChangeset(changes);
|
||||||
|
@ -1582,7 +1585,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
let hasAttrib = true;
|
let hasAttrib = true;
|
||||||
|
|
||||||
let indexIntoLine = 0;
|
let indexIntoLine = 0;
|
||||||
for (const op of Changeset.deserializeOps(rep.alines[lineNum])) {
|
for (const op of deserializeOps(rep.alines[lineNum])) {
|
||||||
const opStartInLine = indexIntoLine;
|
const opStartInLine = indexIntoLine;
|
||||||
const opEndInLine = opStartInLine + op.chars;
|
const opEndInLine = opStartInLine + op.chars;
|
||||||
if (!hasIt(op.attribs)) {
|
if (!hasIt(op.attribs)) {
|
||||||
|
@ -1627,7 +1630,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (n === selEndLine) {
|
if (n === selEndLine) {
|
||||||
selectionEndInLine = rep.selEnd[1];
|
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 opStartInLine = indexIntoLine;
|
||||||
const opEndInLine = opStartInLine + op.chars;
|
const opEndInLine = opStartInLine + op.chars;
|
||||||
if (!hasIt(op.attribs)) {
|
if (!hasIt(op.attribs)) {
|
||||||
|
@ -1745,7 +1748,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
|
const spliceStartLineStart = rep.lines.offsetOfIndex(spliceStartLine);
|
||||||
|
|
||||||
const startBuilder = () => {
|
const startBuilder = () => {
|
||||||
const builder = Changeset.builder(oldLen);
|
const builder = new Builder(oldLen);
|
||||||
builder.keep(spliceStartLineStart, spliceStartLine);
|
builder.keep(spliceStartLineStart, spliceStartLine);
|
||||||
builder.keep(spliceStart - spliceStartLineStart);
|
builder.keep(spliceStart - spliceStartLineStart);
|
||||||
return builder;
|
return builder;
|
||||||
|
@ -1755,7 +1758,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
let textIndex = 0;
|
let textIndex = 0;
|
||||||
const newTextStart = commonStart;
|
const newTextStart = commonStart;
|
||||||
const newTextEnd = newText.length - commonEnd - (shiftFinalNewlineToBeforeNewText ? 1 : 0);
|
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;
|
const nextIndex = textIndex + op.chars;
|
||||||
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
if (!(nextIndex <= newTextStart || textIndex >= newTextEnd)) {
|
||||||
func(Math.max(newTextStart, textIndex), Math.min(newTextEnd, nextIndex), op.attribs);
|
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.
|
// changeset the applies the styles found in the DOM.
|
||||||
// This allows us to incorporate, e.g., Safari's native "unbold".
|
// This allows us to incorporate, e.g., Safari's native "unbold".
|
||||||
const incorpedAttribClearer = cachedStrFunc(
|
const incorpedAttribClearer = cachedStrFunc(
|
||||||
(oldAtts) => Changeset.mapAttribNumbers(oldAtts, (n) => {
|
(oldAtts) => mapAttribNumbers(oldAtts, (n) => {
|
||||||
const k = rep.apool.getAttribKey(n);
|
const k = rep.apool.getAttribKey(n);
|
||||||
if (isStyleAttribute(k)) {
|
if (isStyleAttribute(k)) {
|
||||||
return rep.apool.putAttrib([k, '']);
|
return rep.apool.putAttrib([k, '']);
|
||||||
|
@ -1799,7 +1802,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
});
|
});
|
||||||
const styler = builder2.toString();
|
const styler = builder2.toString();
|
||||||
|
|
||||||
theChangeset = Changeset.compose(clearer, styler, rep.apool);
|
theChangeset = compose(clearer, styler, rep.apool);
|
||||||
} else {
|
} else {
|
||||||
const builder = startBuilder();
|
const builder = startBuilder();
|
||||||
|
|
||||||
|
@ -1869,7 +1872,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const attribRuns = (attribs) => {
|
const attribRuns = (attribs) => {
|
||||||
const lengs = [];
|
const lengs = [];
|
||||||
const atts = [];
|
const atts = [];
|
||||||
for (const op of Changeset.deserializeOps(attribs)) {
|
for (const op of deserializeOps(attribs)) {
|
||||||
lengs.push(op.chars);
|
lengs.push(op.chars);
|
||||||
atts.push(op.attribs);
|
atts.push(op.attribs);
|
||||||
}
|
}
|
||||||
|
@ -1898,8 +1901,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const newLen = newText.length;
|
const newLen = newText.length;
|
||||||
const minLen = Math.min(oldLen, newLen);
|
const minLen = Math.min(oldLen, newLen);
|
||||||
|
|
||||||
const oldARuns = attribRuns(Changeset.filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
const oldARuns = attribRuns(filterAttribNumbers(oldAttribs, incorpedAttribFilter));
|
||||||
const newARuns = attribRuns(Changeset.filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
const newARuns = attribRuns(filterAttribNumbers(newAttribs, incorpedAttribFilter));
|
||||||
|
|
||||||
let commonStart = 0;
|
let commonStart = 0;
|
||||||
const oldStartIter = attribIterator(oldARuns, false);
|
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
|
// 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
|
// 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];
|
let loc = [0, 0];
|
||||||
const applyNumberList = (line, level) => {
|
const applyNumberList = (line, level) => {
|
||||||
// init
|
// init
|
||||||
|
@ -2312,8 +2315,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
if (isNaN(curLevel) || listType[0] === 'indent') {
|
if (isNaN(curLevel) || listType[0] === 'indent') {
|
||||||
return line;
|
return line;
|
||||||
} else if (curLevel === level) {
|
} else if (curLevel === level) {
|
||||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
buildKeepRange(rep, builder, loc, (loc = [line, 0]));
|
||||||
ChangesetUtils.buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
buildKeepRange(rep, builder, loc, (loc = [line, 1]), [
|
||||||
['start', position],
|
['start', position],
|
||||||
], rep.apool);
|
], rep.apool);
|
||||||
|
|
||||||
|
@ -2330,7 +2333,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
|
|
||||||
applyNumberList(lineNum, 1);
|
applyNumberList(lineNum, 1);
|
||||||
const cs = builder.toString();
|
const cs = builder.toString();
|
||||||
if (!Changeset.isIdentity(cs)) {
|
if (!isIdentity(cs)) {
|
||||||
performDocumentApplyChangeset(cs);
|
performDocumentApplyChangeset(cs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2618,7 +2621,7 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
// TODO: There appears to be a race condition or so.
|
// TODO: There appears to be a race condition or so.
|
||||||
const authorIds = new Set();
|
const authorIds = new Set();
|
||||||
if (alineAttrs) {
|
if (alineAttrs) {
|
||||||
for (const op of Changeset.deserializeOps(alineAttrs)) {
|
for (const op of deserializeOps(alineAttrs)) {
|
||||||
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
|
const authorId = AttributeMap.fromString(op.attribs, apool).get('author');
|
||||||
if (authorId) authorIds.add(authorId);
|
if (authorId) authorIds.add(authorId);
|
||||||
}
|
}
|
||||||
|
@ -3513,8 +3516,8 @@ function Ace2Inner(editorInfo, cssManagers) {
|
||||||
const oneEntry = createDomLineEntry('');
|
const oneEntry = createDomLineEntry('');
|
||||||
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
doRepLineSplice(0, rep.lines.length(), [oneEntry]);
|
||||||
insertDomLines(null, [oneEntry.domInfo]);
|
insertDomLines(null, [oneEntry.domInfo]);
|
||||||
rep.alines = Changeset.splitAttributionLines(
|
rep.alines = splitAttributionLines(
|
||||||
Changeset.makeAttribution('\n'), '\n');
|
makeAttribution('\n'), '\n');
|
||||||
|
|
||||||
bindTheEventHandlers();
|
bindTheEventHandlers();
|
||||||
});
|
});
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
const makeCSSManager = require('./cssmanager').makeCSSManager;
|
||||||
const domline = require('./domline').domline;
|
const domline = require('./domline').domline;
|
||||||
import AttribPool from './AttributePool';
|
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 attributes = require('./attributes');
|
||||||
const linestylefilter = require('./linestylefilter').linestylefilter;
|
const linestylefilter = require('./linestylefilter').linestylefilter;
|
||||||
const colorutils = require('./colorutils').colorutils;
|
const colorutils = require('./colorutils').colorutils;
|
||||||
|
@ -54,11 +54,11 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
currentRevision: clientVars.collab_client_vars.rev,
|
currentRevision: clientVars.collab_client_vars.rev,
|
||||||
currentTime: clientVars.collab_client_vars.time,
|
currentTime: clientVars.collab_client_vars.time,
|
||||||
currentLines:
|
currentLines:
|
||||||
Changeset.splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
splitTextLines(clientVars.collab_client_vars.initialAttributedText.text),
|
||||||
currentDivs: null,
|
currentDivs: null,
|
||||||
// to be filled in once the dom loads
|
// to be filled in once the dom loads
|
||||||
apool: (new AttribPool()).fromJsonable(clientVars.collab_client_vars.apool),
|
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.attribs,
|
||||||
clientVars.collab_client_vars.initialAttributedText.text),
|
clientVars.collab_client_vars.initialAttributedText.text),
|
||||||
|
|
||||||
|
@ -121,7 +121,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
getActiveAuthors() {
|
getActiveAuthors() {
|
||||||
const authorIds = new Set();
|
const authorIds = new Set();
|
||||||
for (const aline of this.alines) {
|
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)) {
|
for (const [k, v] of attributes.attribsFromString(op.attribs, this.apool)) {
|
||||||
if (k !== 'author') continue;
|
if (k !== 'author') continue;
|
||||||
if (v) authorIds.add(v);
|
if (v) authorIds.add(v);
|
||||||
|
@ -142,7 +142,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
const oldAlines = padContents.alines.slice();
|
const oldAlines = padContents.alines.slice();
|
||||||
try {
|
try {
|
||||||
// must mutate attribution lines before text lines
|
// must mutate attribution lines before text lines
|
||||||
Changeset.mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
mutateAttributionLines(changeset, padContents.alines, padContents.apool);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugLog(e);
|
debugLog(e);
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
// some chars are replaced (no attributes change and no length change)
|
// some chars are replaced (no attributes change and no length change)
|
||||||
// test if there are keep ops at the start of the cs
|
// test if there are keep ops at the start of the cs
|
||||||
if (lineChanged === undefined) {
|
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;
|
lineChanged = op != null && op.opcode === '=' ? op.lines : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,7 +184,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
goToLineNumber(lineChanged);
|
goToLineNumber(lineChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
Changeset.mutateTextLines(changeset, padContents);
|
mutateTextLines(changeset, padContents);
|
||||||
padContents.currentRevision = revision;
|
padContents.currentRevision = revision;
|
||||||
padContents.currentTime += timeDelta * 1000;
|
padContents.currentTime += timeDelta * 1000;
|
||||||
|
|
||||||
|
@ -273,7 +273,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
let changeset = cs[0];
|
let changeset = cs[0];
|
||||||
let timeDelta = path.times[0];
|
let timeDelta = path.times[0];
|
||||||
for (let i = 1; i < cs.length; i++) {
|
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];
|
timeDelta += path.times[i];
|
||||||
}
|
}
|
||||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
||||||
|
@ -291,7 +291,7 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
let changeset = cs[0];
|
let changeset = cs[0];
|
||||||
let timeDelta = path.times[0];
|
let timeDelta = path.times[0];
|
||||||
for (let i = 1; i < cs.length; i++) {
|
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];
|
timeDelta += path.times[i];
|
||||||
}
|
}
|
||||||
if (changeset) applyChangeset(changeset, path.rev, true, timeDelta);
|
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;
|
if (aend > data.actualEndNum - 1) aend = data.actualEndNum - 1;
|
||||||
// debugLog("adding changeset:", astart, aend);
|
// debugLog("adding changeset:", astart, aend);
|
||||||
const forwardcs =
|
const forwardcs =
|
||||||
Changeset.moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
moveOpsToNewPool(data.forwardsChangesets[i], pool, padContents.apool);
|
||||||
const backwardcs =
|
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]);
|
window.revisionInfo.addChangeset(astart, aend, forwardcs, backwardcs, data.timeDeltas[i]);
|
||||||
}
|
}
|
||||||
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
if (callback) callback(start - 1, start + data.forwardsChangesets.length * granularity - 1);
|
||||||
|
@ -409,13 +409,13 @@ const loadBroadcastJS = (socket, sendSocketMsg, fireWhenAllScriptsAreLoaded, Bro
|
||||||
obj = obj.data;
|
obj = obj.data;
|
||||||
|
|
||||||
if (obj.type === 'NEW_CHANGES') {
|
if (obj.type === 'NEW_CHANGES') {
|
||||||
const changeset = Changeset.moveOpsToNewPool(
|
const changeset = moveOpsToNewPool(
|
||||||
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
obj.changeset, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||||
|
|
||||||
let changesetBack = Changeset.inverse(
|
let changesetBack = inverse(
|
||||||
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
obj.changeset, padContents.currentLines, padContents.alines, padContents.apool);
|
||||||
|
|
||||||
changesetBack = Changeset.moveOpsToNewPool(
|
changesetBack = moveOpsToNewPool(
|
||||||
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
changesetBack, (new AttribPool()).fromJsonable(obj.apool), padContents.apool);
|
||||||
|
|
||||||
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
loadedNewChangeset(changeset, changesetBack, obj.newRev - 1, obj.timeDelta);
|
||||||
|
|
|
@ -25,15 +25,16 @@
|
||||||
|
|
||||||
import AttributeMap from './AttributeMap';
|
import AttributeMap from './AttributeMap';
|
||||||
import AttributePool from './AttributePool';
|
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) => {
|
const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
// latest official text from server
|
// latest official text from server
|
||||||
let baseAText = Changeset.makeAText('\n');
|
let baseAText = makeAText('\n');
|
||||||
// changes applied to baseText that have been submitted
|
// changes applied to baseText that have been submitted
|
||||||
let submittedChangeset = null;
|
let submittedChangeset = null;
|
||||||
// changes applied to submittedChangeset since it was prepared
|
// changes applied to submittedChangeset since it was prepared
|
||||||
let userChangeset = Changeset.identity(1);
|
let userChangeset = identity(1);
|
||||||
// is the changesetTracker enabled
|
// is the changesetTracker enabled
|
||||||
let tracking = false;
|
let tracking = false;
|
||||||
// stack state flag so that when we change the rep we don't
|
// stack state flag so that when we change the rep we don't
|
||||||
|
@ -67,18 +68,18 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
return self = {
|
return self = {
|
||||||
isTracking: () => tracking,
|
isTracking: () => tracking,
|
||||||
setBaseText: (text) => {
|
setBaseText: (text) => {
|
||||||
self.setBaseAttributedText(Changeset.makeAText(text), null);
|
self.setBaseAttributedText(makeAText(text), null);
|
||||||
},
|
},
|
||||||
setBaseAttributedText: (atext, apoolJsonObj) => {
|
setBaseAttributedText: (atext, apoolJsonObj) => {
|
||||||
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
|
aceCallbacksProvider.withCallbacks('setBaseText', (callbacks) => {
|
||||||
tracking = true;
|
tracking = true;
|
||||||
baseAText = Changeset.cloneAText(atext);
|
baseAText = cloneAText(atext);
|
||||||
if (apoolJsonObj) {
|
if (apoolJsonObj) {
|
||||||
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
const wireApool = (new AttributePool()).fromJsonable(apoolJsonObj);
|
||||||
baseAText.attribs = Changeset.moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
baseAText.attribs = moveOpsToNewPool(baseAText.attribs, wireApool, apool);
|
||||||
}
|
}
|
||||||
submittedChangeset = null;
|
submittedChangeset = null;
|
||||||
userChangeset = Changeset.identity(atext.text.length);
|
userChangeset = identity(atext.text.length);
|
||||||
applyingNonUserChanges = true;
|
applyingNonUserChanges = true;
|
||||||
try {
|
try {
|
||||||
callbacks.setDocumentAttributedText(atext);
|
callbacks.setDocumentAttributedText(atext);
|
||||||
|
@ -90,8 +91,8 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
composeUserChangeset: (c) => {
|
composeUserChangeset: (c) => {
|
||||||
if (!tracking) return;
|
if (!tracking) return;
|
||||||
if (applyingNonUserChanges) return;
|
if (applyingNonUserChanges) return;
|
||||||
if (Changeset.isIdentity(c)) return;
|
if (isIdentity(c)) return;
|
||||||
userChangeset = Changeset.compose(userChangeset, c, apool);
|
userChangeset = compose(userChangeset, c, apool);
|
||||||
|
|
||||||
setChangeCallbackTimeout();
|
setChangeCallbackTimeout();
|
||||||
},
|
},
|
||||||
|
@ -101,23 +102,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
|
aceCallbacksProvider.withCallbacks('applyChangesToBase', (callbacks) => {
|
||||||
if (apoolJsonObj) {
|
if (apoolJsonObj) {
|
||||||
const wireApool = (new AttributePool()).fromJsonable(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;
|
let c2 = c;
|
||||||
if (submittedChangeset) {
|
if (submittedChangeset) {
|
||||||
const oldSubmittedChangeset = submittedChangeset;
|
const oldSubmittedChangeset = submittedChangeset;
|
||||||
submittedChangeset = Changeset.follow(c, oldSubmittedChangeset, false, apool);
|
submittedChangeset = follow(c, oldSubmittedChangeset, false, apool);
|
||||||
c2 = Changeset.follow(oldSubmittedChangeset, c, true, apool);
|
c2 = follow(oldSubmittedChangeset, c, true, apool);
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferInsertingAfterUserChanges = true;
|
const preferInsertingAfterUserChanges = true;
|
||||||
const oldUserChangeset = userChangeset;
|
const oldUserChangeset = userChangeset;
|
||||||
userChangeset = Changeset.follow(
|
userChangeset = follow(
|
||||||
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
c2, oldUserChangeset, preferInsertingAfterUserChanges, apool);
|
||||||
const postChange = Changeset.follow(
|
const postChange = follow(
|
||||||
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
oldUserChangeset, c2, !preferInsertingAfterUserChanges, apool);
|
||||||
|
|
||||||
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
|
const preferInsertionAfterCaret = true; // (optAuthor && optAuthor > thisAuthor);
|
||||||
|
@ -136,17 +137,17 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
if (submittedChangeset) {
|
if (submittedChangeset) {
|
||||||
// submission must have been canceled, prepare new changeset
|
// submission must have been canceled, prepare new changeset
|
||||||
// that includes old submittedChangeset
|
// that includes old submittedChangeset
|
||||||
toSubmit = Changeset.compose(submittedChangeset, userChangeset, apool);
|
toSubmit = compose(submittedChangeset, userChangeset, apool);
|
||||||
} else {
|
} else {
|
||||||
// Get my authorID
|
// Get my authorID
|
||||||
const authorId = parent.parent.pad.myUserInfo.userId;
|
const authorId = parent.parent.pad.myUserInfo.userId;
|
||||||
|
|
||||||
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
// Sanitize authorship: Replace all author attributes with this user's author ID in case the
|
||||||
// text was copied from another author.
|
// text was copied from another author.
|
||||||
const cs = Changeset.unpack(userChangeset);
|
const cs = unpack(userChangeset);
|
||||||
const assem = Changeset.mergingOpAssembler();
|
const assem = new MergingOpAssembler();
|
||||||
|
|
||||||
for (const op of Changeset.deserializeOps(cs.ops)) {
|
for (const op of deserializeOps(cs.ops)) {
|
||||||
if (op.opcode === '+') {
|
if (op.opcode === '+') {
|
||||||
const attribs = AttributeMap.fromString(op.attribs, apool);
|
const attribs = AttributeMap.fromString(op.attribs, apool);
|
||||||
const oldAuthorId = attribs.get('author');
|
const oldAuthorId = attribs.get('author');
|
||||||
|
@ -158,23 +159,23 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
assem.append(op);
|
assem.append(op);
|
||||||
}
|
}
|
||||||
assem.endDocument();
|
assem.endDocument();
|
||||||
userChangeset = Changeset.pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
userChangeset = pack(cs.oldLen, cs.newLen, assem.toString(), cs.charBank);
|
||||||
Changeset.checkRep(userChangeset);
|
checkRep(userChangeset);
|
||||||
|
|
||||||
if (Changeset.isIdentity(userChangeset)) toSubmit = null;
|
if (isIdentity(userChangeset)) toSubmit = null;
|
||||||
else toSubmit = userChangeset;
|
else toSubmit = userChangeset;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cs = null;
|
let cs = null;
|
||||||
if (toSubmit) {
|
if (toSubmit) {
|
||||||
submittedChangeset = toSubmit;
|
submittedChangeset = toSubmit;
|
||||||
userChangeset = Changeset.identity(Changeset.newLen(toSubmit));
|
userChangeset = identity(newLen(toSubmit));
|
||||||
|
|
||||||
cs = toSubmit;
|
cs = toSubmit;
|
||||||
}
|
}
|
||||||
let wireApool = null;
|
let wireApool = null;
|
||||||
if (cs) {
|
if (cs) {
|
||||||
const forWire = Changeset.prepareForWire(cs, apool);
|
const forWire = prepareForWire(cs, apool);
|
||||||
wireApool = forWire.pool.toJsonable();
|
wireApool = forWire.pool.toJsonable();
|
||||||
cs = forWire.translated;
|
cs = forWire.translated;
|
||||||
}
|
}
|
||||||
|
@ -191,13 +192,13 @@ const makeChangesetTracker = (scheduler, apool, aceCallbacksProvider) => {
|
||||||
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
|
throw new Error('applySubmittedChangesToBase: no submitted changes to apply');
|
||||||
}
|
}
|
||||||
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
// bumpDebug("applying committed changeset: "+submittedChangeset.encodeToString(false));
|
||||||
baseAText = Changeset.applyToAText(submittedChangeset, baseAText, apool);
|
baseAText = applyToAText(submittedChangeset, baseAText, apool);
|
||||||
submittedChangeset = null;
|
submittedChangeset = null;
|
||||||
},
|
},
|
||||||
setUserChangeNotificationCallback: (callback) => {
|
setUserChangeNotificationCallback: (callback) => {
|
||||||
changeCallback = callback;
|
changeCallback = callback;
|
||||||
},
|
},
|
||||||
hasUncommittedChanges: () => !!(submittedChangeset || (!Changeset.isIdentity(userChangeset))),
|
hasUncommittedChanges: () => !!(submittedChangeset || (!isIdentity(userChangeset))),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
/**
|
/**
|
||||||
* This code is mostly from the old Etherpad. Please help us to comment this code.
|
* 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
|
// THIS FILE IS ALSO AN APPJET MODULE: etherpad.collab.ace.contentcollector
|
||||||
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
// %APPJET%: import("etherpad.collab.ace.easysync2.Changeset");
|
||||||
// %APPJET%: import("etherpad.admin.plugins");
|
// %APPJET%: import("etherpad.admin.plugins");
|
||||||
|
import Op from "./Op";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copyright 2009 Google Inc.
|
* Copyright 2009 Google Inc.
|
||||||
*
|
*
|
||||||
|
@ -28,8 +31,9 @@
|
||||||
const _MAX_LIST_LEVEL = 16;
|
const _MAX_LIST_LEVEL = 16;
|
||||||
|
|
||||||
import AttributeMap from './AttributeMap';
|
import AttributeMap from './AttributeMap';
|
||||||
const UNorm = require('unorm');
|
import UNorm from 'unorm';
|
||||||
const Changeset = require('./Changeset');
|
import {subattribution} from './Changeset';
|
||||||
|
import {SmartOpAssembler} from "./SmartOpAssembler";
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
|
|
||||||
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
const sanitizeUnicode = (s) => UNorm.nfc(s);
|
||||||
|
@ -84,14 +88,14 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
const textArray = [];
|
const textArray = [];
|
||||||
const attribsArray = [];
|
const attribsArray = [];
|
||||||
let attribsBuilder = null;
|
let attribsBuilder = null;
|
||||||
const op = new Changeset.Op('+');
|
const op = new Op('+');
|
||||||
const self = {
|
const self = {
|
||||||
length: () => textArray.length,
|
length: () => textArray.length,
|
||||||
atColumnZero: () => textArray[textArray.length - 1] === '',
|
atColumnZero: () => textArray[textArray.length - 1] === '',
|
||||||
startNew: () => {
|
startNew: () => {
|
||||||
textArray.push('');
|
textArray.push('');
|
||||||
self.flush(true);
|
self.flush(true);
|
||||||
attribsBuilder = Changeset.smartOpAssembler();
|
attribsBuilder = new SmartOpAssembler();
|
||||||
},
|
},
|
||||||
textOfLine: (i) => textArray[i],
|
textOfLine: (i) => textArray[i],
|
||||||
appendText: (txt, attrString = '') => {
|
appendText: (txt, attrString = '') => {
|
||||||
|
@ -654,8 +658,8 @@ const makeContentCollector = (collectStyles, abrowser, apool, className2Author)
|
||||||
const lengthToTake = lineLimit;
|
const lengthToTake = lineLimit;
|
||||||
newStrings.push(oldString.substring(0, lengthToTake));
|
newStrings.push(oldString.substring(0, lengthToTake));
|
||||||
oldString = oldString.substring(lengthToTake);
|
oldString = oldString.substring(lengthToTake);
|
||||||
newAttribStrings.push(Changeset.subattribution(oldAttribString, 0, lengthToTake));
|
newAttribStrings.push(subattribution(oldAttribString, 0, lengthToTake));
|
||||||
oldAttribString = Changeset.subattribution(oldAttribString, lengthToTake);
|
oldAttribString = subattribution(oldAttribString, lengthToTake);
|
||||||
}
|
}
|
||||||
if (oldString.length > 0) {
|
if (oldString.length > 0) {
|
||||||
newStrings.push(oldString);
|
newStrings.push(oldString);
|
||||||
|
|
|
@ -31,12 +31,13 @@
|
||||||
// requires: plugins
|
// requires: plugins
|
||||||
// requires: undefined
|
// requires: undefined
|
||||||
|
|
||||||
const Changeset = require('./Changeset');
|
import {deserializeOps} from './Changeset';
|
||||||
const attributes = require('./attributes');
|
import attributes from './attributes';
|
||||||
const hooks = require('./pluginfw/hooks');
|
const hooks = require('./pluginfw/hooks');
|
||||||
const linestylefilter = {};
|
const linestylefilter = {};
|
||||||
const AttributeManager = require('./AttributeManager');
|
const AttributeManager = require('./AttributeManager');
|
||||||
import padutils from './pad_utils'
|
import padutils from './pad_utils'
|
||||||
|
import Op from "./Op";
|
||||||
|
|
||||||
linestylefilter.ATTRIB_CLASSES = {
|
linestylefilter.ATTRIB_CLASSES = {
|
||||||
bold: 'tag:b',
|
bold: 'tag:b',
|
||||||
|
@ -99,12 +100,12 @@ linestylefilter.getLineStyleFilter = (lineLength, aline, textAndClassFunc, apool
|
||||||
return classes.substring(1);
|
return classes.substring(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const attrOps = Changeset.deserializeOps(aline);
|
const attrOps = deserializeOps(aline);
|
||||||
let attrOpsNext = attrOps.next();
|
let attrOpsNext = attrOps.next();
|
||||||
let nextOp, nextOpClasses;
|
let nextOp, nextOpClasses;
|
||||||
|
|
||||||
const goNextOp = () => {
|
const goNextOp = () => {
|
||||||
nextOp = attrOpsNext.done ? new Changeset.Op() : attrOpsNext.value;
|
nextOp = attrOpsNext.done ? new Op() : attrOpsNext.value;
|
||||||
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
|
if (!attrOpsNext.done) attrOpsNext = attrOps.next();
|
||||||
nextOpClasses = (nextOp.opcode && attribsToClasses(nextOp.attribs));
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const Changeset = require('./Changeset');
|
import {characterRangeFollow, compose, follow, isIdentity, unpack} from './Changeset';
|
||||||
const _ = require('./underscore');
|
const _ = require('./underscore');
|
||||||
|
|
||||||
const undoModule = (() => {
|
const undoModule = (() => {
|
||||||
|
@ -62,7 +62,7 @@ const undoModule = (() => {
|
||||||
const idx = stackElements.length - 1;
|
const idx = stackElements.length - 1;
|
||||||
if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
|
if (stackElements[idx].elementType === EXTERNAL_CHANGE) {
|
||||||
stackElements[idx].changeset =
|
stackElements[idx].changeset =
|
||||||
Changeset.compose(stackElements[idx].changeset, cs, getAPool());
|
compose(stackElements[idx].changeset, cs, getAPool());
|
||||||
} else {
|
} else {
|
||||||
stackElements.push(
|
stackElements.push(
|
||||||
{
|
{
|
||||||
|
@ -83,10 +83,10 @@ const undoModule = (() => {
|
||||||
if (un.backset) {
|
if (un.backset) {
|
||||||
const excs = ex.changeset;
|
const excs = ex.changeset;
|
||||||
const unbs = un.backset;
|
const unbs = un.backset;
|
||||||
un.backset = Changeset.follow(excs, un.backset, false, getAPool());
|
un.backset = follow(excs, un.backset, false, getAPool());
|
||||||
ex.changeset = Changeset.follow(unbs, ex.changeset, true, getAPool());
|
ex.changeset = follow(unbs, ex.changeset, true, getAPool());
|
||||||
if ((typeof un.selStart) === 'number') {
|
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.selStart = newSel[0];
|
||||||
un.selEnd = newSel[1];
|
un.selEnd = newSel[1];
|
||||||
if (un.selStart === un.selEnd) {
|
if (un.selStart === un.selEnd) {
|
||||||
|
@ -98,7 +98,7 @@ const undoModule = (() => {
|
||||||
stackElements[idx] = un;
|
stackElements[idx] = un;
|
||||||
if (idx >= 2 && stackElements[idx - 2].elementType === EXTERNAL_CHANGE) {
|
if (idx >= 2 && stackElements[idx - 2].elementType === EXTERNAL_CHANGE) {
|
||||||
ex.changeset =
|
ex.changeset =
|
||||||
Changeset.compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
|
compose(stackElements[idx - 2].changeset, ex.changeset, getAPool());
|
||||||
stackElements.splice(idx - 2, 1);
|
stackElements.splice(idx - 2, 1);
|
||||||
idx--;
|
idx--;
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ const undoModule = (() => {
|
||||||
return count;
|
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) => {
|
const _mergeChangesets = (cs1, cs2) => {
|
||||||
if (!cs1) return cs2;
|
if (!cs1) return cs2;
|
||||||
|
@ -171,14 +171,14 @@ const undoModule = (() => {
|
||||||
const minusCount1 = _opcodeOccurrences(cs1, '-');
|
const minusCount1 = _opcodeOccurrences(cs1, '-');
|
||||||
const minusCount2 = _opcodeOccurrences(cs2, '-');
|
const minusCount2 = _opcodeOccurrences(cs2, '-');
|
||||||
if (plusCount1 === 1 && plusCount2 === 1 && minusCount1 === 0 && minusCount2 === 0) {
|
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 plusCount3 = _opcodeOccurrences(merge, '+');
|
||||||
const minusCount3 = _opcodeOccurrences(merge, '-');
|
const minusCount3 = _opcodeOccurrences(merge, '-');
|
||||||
if (plusCount3 === 1 && minusCount3 === 0) {
|
if (plusCount3 === 1 && minusCount3 === 0) {
|
||||||
return merge;
|
return merge;
|
||||||
}
|
}
|
||||||
} else if (plusCount1 === 0 && plusCount2 === 0 && minusCount1 === 1 && minusCount2 === 1) {
|
} 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 plusCount3 = _opcodeOccurrences(merge, '+');
|
||||||
const minusCount3 = _opcodeOccurrences(merge, '-');
|
const minusCount3 = _opcodeOccurrences(merge, '-');
|
||||||
if (plusCount3 === 0 && minusCount3 === 1) {
|
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();
|
applySelectionToTop();
|
||||||
} else {
|
} else {
|
||||||
let merged = false;
|
let merged = false;
|
||||||
|
@ -227,7 +227,7 @@ const undoModule = (() => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const reportExternalChange = (changeset) => {
|
const reportExternalChange = (changeset) => {
|
||||||
if (changeset && !Changeset.isIdentity(changeset)) {
|
if (changeset && !isIdentity(changeset)) {
|
||||||
stack.pushExternalChange(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({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
include: ["tests/backend-new/**/*.ts"],
|
include: ["tests/backend-new/specs/**/*.ts"],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue