mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-15 06:33:55 +01:00
[overleaf-editor-core] Restructure TextOperation hierachy (#16582)
* [overleaf-editor-core] Restructure TextOperation hierachy Restructures the hierachy of TextOperations to include a superclass EditOperation. This superclass will later on contain other classes used for tracked changes and comments. * [overleaf-editor-core] Update json format of LazyStringFileData * [history-v1+project-history] Fix TextOperation.fromJSON calls * [overleaf-editor-core] Change EditOperationBuilder.fromRaw to fromJSON * [overleaf-editor-core] Update apply and invert functions to accept FileData * [overleaf-editor-core] Pass missing argument to store method * [overleaf-editor-core] Remove unused method * [overleaf-editor-core] User EditOperationTransformer * [overleaf-editor-core] Clean up JSDoc comments * [overleaf-editor-core] Add tests for EditOperation * [overleaf-editor-core] Update JSDoc types GitOrigin-RevId: 9c22a3a89b8483bdb87b43f329ddbdd887ffed42
This commit is contained in:
committed by
Copybot
parent
fc90db231c
commit
43b2fe4a3a
@@ -1,17 +1,18 @@
|
||||
// @ts-check
|
||||
'use strict'
|
||||
|
||||
const { expect } = require('chai')
|
||||
|
||||
const ot = require('..')
|
||||
const EditOperationBuilder = require('../lib/operation/edit_operation_builder')
|
||||
const File = ot.File
|
||||
const Operation = ot.Operation
|
||||
const TextOperation = ot.TextOperation
|
||||
|
||||
describe('EditFileOperation', function () {
|
||||
function edit(pathname, textOperationJsonObject) {
|
||||
return Operation.editFile(
|
||||
pathname,
|
||||
TextOperation.fromJSON(textOperationJsonObject)
|
||||
EditOperationBuilder.fromJSON({ textOperation: textOperationJsonObject })
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
const { expect } = require('chai')
|
||||
const EditOperationBuilder = require('../lib/operation/edit_operation_builder')
|
||||
const TextOperation = require('../lib/operation/text_operation')
|
||||
const EditOperationTransformer = require('../lib/operation/edit_operation_transformer')
|
||||
const EditOperation = require('../lib/operation/edit_operation')
|
||||
const randomTextOperation = require('./support/random_text_operation')
|
||||
const random = require('./support/random')
|
||||
|
||||
describe('EditOperation', function () {
|
||||
it('Cannot be instantiated', function () {
|
||||
expect(() => new EditOperation()).to.throw(
|
||||
'Cannot instantiate abstract class'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EditOperationTransformer', function () {
|
||||
it('Transforms two TextOperations', function () {
|
||||
const a = new TextOperation().insert('foo')
|
||||
const b = new TextOperation().insert('bar')
|
||||
const [aPrime, bPrime] = EditOperationTransformer.transform(a, b)
|
||||
expect(aPrime).to.be.an.instanceof(TextOperation)
|
||||
expect(bPrime).to.be.an.instanceof(TextOperation)
|
||||
})
|
||||
})
|
||||
|
||||
describe('EditOperationBuilder', function () {
|
||||
it('Constructs TextOperation from JSON', function () {
|
||||
const raw = {
|
||||
textOperation: [1, 'foo', 3],
|
||||
}
|
||||
const op = EditOperationBuilder.fromJSON(raw)
|
||||
expect(op).to.be.an.instanceof(TextOperation)
|
||||
expect(op.ops).to.deep.equal([1, 'foo', 3])
|
||||
})
|
||||
|
||||
it('Throws error for unsupported operation', function () {
|
||||
const raw = {
|
||||
unsupportedOperation: {
|
||||
op: 'foo',
|
||||
},
|
||||
}
|
||||
expect(() => EditOperationBuilder.fromJSON(raw)).to.throw(
|
||||
'Unsupported operation in EditOperationBuilder.fromJSON'
|
||||
)
|
||||
})
|
||||
|
||||
it('Constructs TextOperation from JSON (randomised)', function () {
|
||||
const str = random.string(50)
|
||||
const randomOperation = randomTextOperation(str)
|
||||
const op = EditOperationBuilder.fromJSON(randomOperation.toJSON())
|
||||
expect(op).to.be.an.instanceof(TextOperation)
|
||||
expect(op.equals(randomOperation)).to.be.true
|
||||
})
|
||||
})
|
||||
@@ -21,32 +21,32 @@ describe('LazyStringFileData', function () {
|
||||
roundTripFileData = LazyStringFileData.fromRaw(fileData.toRaw())
|
||||
expect(roundTripFileData.getHash()).to.equal(testHash)
|
||||
expect(roundTripFileData.getStringLength()).to.equal(0)
|
||||
expect(roundTripFileData.getTextOperations()).to.have.length(0)
|
||||
expect(roundTripFileData.getOperations()).to.have.length(0)
|
||||
|
||||
fileData.edit(new TextOperation().insert('a'))
|
||||
expect(fileData.toRaw()).to.eql({
|
||||
hash: testHash,
|
||||
stringLength: 1,
|
||||
textOperations: [['a']],
|
||||
operations: [{ textOperation: ['a'] }],
|
||||
})
|
||||
roundTripFileData = LazyStringFileData.fromRaw(fileData.toRaw())
|
||||
expect(roundTripFileData.getHash()).not.to.exist // file has changed
|
||||
expect(roundTripFileData.getStringLength()).to.equal(1)
|
||||
expect(roundTripFileData.getTextOperations()).to.have.length(1)
|
||||
expect(roundTripFileData.getTextOperations()[0].ops).to.have.length(1)
|
||||
expect(roundTripFileData.getOperations()).to.have.length(1)
|
||||
expect(roundTripFileData.getOperations()[0].ops).to.have.length(1)
|
||||
|
||||
fileData.edit(new TextOperation().retain(1).insert('b'))
|
||||
expect(fileData.toRaw()).to.eql({
|
||||
hash: testHash,
|
||||
stringLength: 2,
|
||||
textOperations: [['a'], [1, 'b']],
|
||||
operations: [{ textOperation: ['a'] }, { textOperation: [1, 'b'] }],
|
||||
})
|
||||
roundTripFileData = LazyStringFileData.fromRaw(fileData.toRaw())
|
||||
expect(roundTripFileData.getHash()).not.to.exist // file has changed
|
||||
expect(roundTripFileData.getStringLength()).to.equal(2)
|
||||
expect(roundTripFileData.getTextOperations()).to.have.length(2)
|
||||
expect(roundTripFileData.getTextOperations()[0].ops).to.have.length(1)
|
||||
expect(roundTripFileData.getTextOperations()[1].ops).to.have.length(2)
|
||||
expect(roundTripFileData.getOperations()).to.have.length(2)
|
||||
expect(roundTripFileData.getOperations()[0].ops).to.have.length(1)
|
||||
expect(roundTripFileData.getOperations()[1].ops).to.have.length(2)
|
||||
})
|
||||
|
||||
it('validates operations when edited', function () {
|
||||
@@ -55,13 +55,13 @@ describe('LazyStringFileData', function () {
|
||||
expect(fileData.getHash()).equal(testHash)
|
||||
expect(fileData.getByteLength()).to.equal(0) // approximately
|
||||
expect(fileData.getStringLength()).to.equal(0)
|
||||
expect(fileData.getTextOperations()).to.have.length(0)
|
||||
expect(fileData.getOperations()).to.have.length(0)
|
||||
|
||||
fileData.edit(new TextOperation().insert('a'))
|
||||
expect(fileData.getHash()).not.to.exist
|
||||
expect(fileData.getByteLength()).to.equal(1) // approximately
|
||||
expect(fileData.getStringLength()).to.equal(1)
|
||||
expect(fileData.getTextOperations()).to.have.length(1)
|
||||
expect(fileData.getOperations()).to.have.length(1)
|
||||
|
||||
expect(() => {
|
||||
fileData.edit(new TextOperation().retain(10))
|
||||
@@ -69,7 +69,7 @@ describe('LazyStringFileData', function () {
|
||||
expect(fileData.getHash()).not.to.exist
|
||||
expect(fileData.getByteLength()).to.equal(1) // approximately
|
||||
expect(fileData.getStringLength()).to.equal(1)
|
||||
expect(fileData.getTextOperations()).to.have.length(1)
|
||||
expect(fileData.getOperations()).to.have.length(1)
|
||||
})
|
||||
|
||||
it('validates string length when edited', function () {
|
||||
@@ -78,14 +78,14 @@ describe('LazyStringFileData', function () {
|
||||
expect(fileData.getHash()).equal(testHash)
|
||||
expect(fileData.getByteLength()).to.equal(0) // approximately
|
||||
expect(fileData.getStringLength()).to.equal(0)
|
||||
expect(fileData.getTextOperations()).to.have.length(0)
|
||||
expect(fileData.getOperations()).to.have.length(0)
|
||||
|
||||
const longString = _.repeat('a', TextOperation.MAX_STRING_LENGTH)
|
||||
fileData.edit(new TextOperation().insert(longString))
|
||||
expect(fileData.getHash()).not.to.exist
|
||||
expect(fileData.getByteLength()).to.equal(longString.length) // approximate
|
||||
expect(fileData.getStringLength()).to.equal(longString.length)
|
||||
expect(fileData.getTextOperations()).to.have.length(1)
|
||||
expect(fileData.getOperations()).to.have.length(1)
|
||||
|
||||
expect(() => {
|
||||
fileData.edit(new TextOperation().retain(longString.length).insert('x'))
|
||||
@@ -93,6 +93,6 @@ describe('LazyStringFileData', function () {
|
||||
expect(fileData.getHash()).not.to.exist
|
||||
expect(fileData.getByteLength()).to.equal(longString.length) // approximate
|
||||
expect(fileData.getStringLength()).to.equal(longString.length)
|
||||
expect(fileData.getTextOperations()).to.have.length(1)
|
||||
expect(fileData.getOperations()).to.have.length(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,10 +114,10 @@ describe('Operation', function () {
|
||||
}
|
||||
|
||||
// shorthand for creating an edit operation
|
||||
function edit(pathname, textOperationJsonObject) {
|
||||
function edit(pathname, textOperationOps) {
|
||||
return Operation.editFile(
|
||||
pathname,
|
||||
TextOperation.fromJSON(textOperationJsonObject)
|
||||
TextOperation.fromJSON({ textOperation: textOperationOps })
|
||||
)
|
||||
}
|
||||
|
||||
@@ -633,14 +633,12 @@ describe('Operation', function () {
|
||||
.expectPrime(1, EditFileOperation)
|
||||
.expectPrime(0, EditFileOperation)
|
||||
.tap(function () {
|
||||
expect(this.primeOperations[1].getTextOperation().toJSON()).to.eql([
|
||||
1,
|
||||
'y',
|
||||
])
|
||||
expect(this.primeOperations[0].getTextOperation().toJSON()).to.eql([
|
||||
'x',
|
||||
1,
|
||||
])
|
||||
expect(this.primeOperations[1].getOperation().toJSON()).to.eql({
|
||||
textOperation: [1, 'y'],
|
||||
})
|
||||
expect(this.primeOperations[0].getOperation().toJSON()).to.eql({
|
||||
textOperation: ['x', 1],
|
||||
})
|
||||
})
|
||||
.swap()
|
||||
.expectFiles({ foo: 'yx' })
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const TextOperation = require('../../lib/operation/text_operation')
|
||||
const random = require('./random')
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {TextOperation}
|
||||
*/
|
||||
function randomTextOperation(str) {
|
||||
const operation = new TextOperation()
|
||||
let left
|
||||
while (true) {
|
||||
left = str.length - operation.baseLength
|
||||
if (left === 0) break
|
||||
const r = Math.random()
|
||||
const l = 1 + random.int(Math.min(left - 1, 20))
|
||||
if (r < 0.2) {
|
||||
operation.insert(random.string(l))
|
||||
} else if (r < 0.4) {
|
||||
operation.remove(l)
|
||||
} else {
|
||||
operation.retain(l)
|
||||
}
|
||||
}
|
||||
if (Math.random() < 0.3) {
|
||||
operation.insert(1 + random.string(10))
|
||||
}
|
||||
return operation
|
||||
}
|
||||
|
||||
module.exports = randomTextOperation
|
||||
@@ -7,31 +7,11 @@
|
||||
|
||||
const { expect } = require('chai')
|
||||
const random = require('./support/random')
|
||||
const randomOperation = require('./support/random_text_operation')
|
||||
|
||||
const ot = require('..')
|
||||
const TextOperation = ot.TextOperation
|
||||
|
||||
function randomOperation(str) {
|
||||
const operation = new TextOperation()
|
||||
let left
|
||||
while (true) {
|
||||
left = str.length - operation.baseLength
|
||||
if (left === 0) break
|
||||
const r = Math.random()
|
||||
const l = 1 + random.int(Math.min(left - 1, 20))
|
||||
if (r < 0.2) {
|
||||
operation.insert(random.string(l))
|
||||
} else if (r < 0.4) {
|
||||
operation.remove(l)
|
||||
} else {
|
||||
operation.retain(l)
|
||||
}
|
||||
}
|
||||
if (Math.random() < 0.3) {
|
||||
operation.insert(1 + random.string(10))
|
||||
}
|
||||
return operation
|
||||
}
|
||||
const StringFileData = require('../lib/file_data/string_file_data')
|
||||
|
||||
describe('TextOperation', function () {
|
||||
const numTrials = 500
|
||||
@@ -134,7 +114,7 @@ describe('TextOperation', function () {
|
||||
|
||||
it('converts from JSON', function () {
|
||||
const ops = [2, -1, -1, 'cde']
|
||||
const o = TextOperation.fromJSON(ops)
|
||||
const o = TextOperation.fromJSON({ textOperation: ops })
|
||||
expect(o.ops.length).to.equal(3)
|
||||
expect(o.baseLength).to.equal(4)
|
||||
expect(o.targetLength).to.equal(5)
|
||||
@@ -143,7 +123,7 @@ describe('TextOperation', function () {
|
||||
const ops2 = ops.slice(0)
|
||||
fn(ops2)
|
||||
expect(() => {
|
||||
TextOperation.fromJSON(ops2)
|
||||
TextOperation.fromJSON({ textOperations: ops2 })
|
||||
}).to.throw
|
||||
}
|
||||
|
||||
@@ -161,7 +141,10 @@ describe('TextOperation', function () {
|
||||
const str = random.string(50)
|
||||
const o = randomOperation(str)
|
||||
expect(str.length).to.equal(o.baseLength)
|
||||
expect(o.apply(str).length).to.equal(o.targetLength)
|
||||
const file = new StringFileData(str)
|
||||
o.apply(file)
|
||||
const result = file.getContent()
|
||||
expect(result.length).to.equal(o.targetLength)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -170,10 +153,14 @@ describe('TextOperation', function () {
|
||||
random.test(numTrials, () => {
|
||||
const str = random.string(50)
|
||||
const o = randomOperation(str)
|
||||
const p = o.invert(str)
|
||||
const p = o.invert(new StringFileData(str))
|
||||
expect(o.baseLength).to.equal(p.targetLength)
|
||||
expect(o.targetLength).to.equal(p.baseLength)
|
||||
expect(p.apply(o.apply(str))).to.equal(str)
|
||||
const file = new StringFileData(str)
|
||||
o.apply(file)
|
||||
p.apply(file)
|
||||
const result = file.getContent()
|
||||
expect(result).to.equal(str)
|
||||
})
|
||||
)
|
||||
|
||||
@@ -193,14 +180,18 @@ describe('TextOperation', function () {
|
||||
// invariant: apply(str, compose(a, b)) === apply(apply(str, a), b)
|
||||
const str = random.string(20)
|
||||
const a = randomOperation(str)
|
||||
const afterA = a.apply(str)
|
||||
const file = new StringFileData(str)
|
||||
a.apply(file)
|
||||
const afterA = file.getContent()
|
||||
expect(afterA.length).to.equal(a.targetLength)
|
||||
const b = randomOperation(afterA)
|
||||
const afterB = b.apply(afterA)
|
||||
b.apply(file)
|
||||
const afterB = file.getContent()
|
||||
expect(afterB.length).to.equal(b.targetLength)
|
||||
const ab = a.compose(b)
|
||||
expect(ab.targetLength).to.equal(b.targetLength)
|
||||
const afterAB = ab.apply(str)
|
||||
ab.apply(new StringFileData(str))
|
||||
const afterAB = file.getContent()
|
||||
expect(afterAB).to.equal(afterB)
|
||||
})
|
||||
)
|
||||
@@ -218,20 +209,22 @@ describe('TextOperation', function () {
|
||||
const bPrime = primes[1]
|
||||
const abPrime = a.compose(bPrime)
|
||||
const baPrime = b.compose(aPrime)
|
||||
const afterAbPrime = abPrime.apply(str)
|
||||
const afterBaPrime = baPrime.apply(str)
|
||||
const abFile = new StringFileData(str)
|
||||
const baFile = new StringFileData(str)
|
||||
abPrime.apply(abFile)
|
||||
baPrime.apply(baFile)
|
||||
expect(abPrime.equals(baPrime)).to.be.true
|
||||
expect(afterAbPrime).to.equal(afterBaPrime)
|
||||
expect(abFile.getContent()).to.equal(baFile.getContent())
|
||||
})
|
||||
)
|
||||
|
||||
it('throws when invalid operations are applied', function () {
|
||||
const operation = new TextOperation().retain(1)
|
||||
expect(() => {
|
||||
operation.apply('')
|
||||
operation.apply(new StringFileData(''))
|
||||
}).to.throw(TextOperation.ApplyError)
|
||||
expect(() => {
|
||||
operation.apply(' ')
|
||||
operation.apply(new StringFileData(' '))
|
||||
}).not.to.throw
|
||||
})
|
||||
|
||||
@@ -250,7 +243,7 @@ describe('TextOperation', function () {
|
||||
const operation = new TextOperation()
|
||||
const str = '𝌆\n'
|
||||
expect(() => {
|
||||
operation.apply(str)
|
||||
operation.apply(new StringFileData(str))
|
||||
}).to.throw(
|
||||
TextOperation.UnprocessableError,
|
||||
/string contains non BMP characters/
|
||||
@@ -260,7 +253,7 @@ describe('TextOperation', function () {
|
||||
it('throws at from JSON when it contains non BMP chars', function () {
|
||||
const operation = ['𝌆\n']
|
||||
expect(() => {
|
||||
TextOperation.fromJSON(operation)
|
||||
TextOperation.fromJSON({ textOperation: operation })
|
||||
}).to.throw(
|
||||
TextOperation.UnprocessableError,
|
||||
/inserted text contains non BMP characters/
|
||||
|
||||
Reference in New Issue
Block a user