mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-14 20:40:17 -05:00
Add in diff generating functions
This commit is contained in:
parent
76cdd5cf98
commit
e0402692cf
3 changed files with 408 additions and 2 deletions
|
@ -55,12 +55,12 @@ module.exports = (grunt) ->
|
|||
|
||||
mochaTest:
|
||||
unit:
|
||||
src: ['test/unit/js/**/*.js']
|
||||
src: ["test/unit/js/#{grunt.option("feature") or "**"}/*.js"]
|
||||
options:
|
||||
reporter: grunt.option('reporter') or 'spec'
|
||||
grep: grunt.option("grep")
|
||||
acceptance:
|
||||
src: ['test/acceptance/js/**/*.js']
|
||||
src: ["test/acceptance/js/**/*.js"]
|
||||
options:
|
||||
reporter: grunt.option('reporter') or 'spec'
|
||||
grep: grunt.option("grep")
|
||||
|
|
168
services/track-changes/app/coffee/DiffGenerator.coffee
Normal file
168
services/track-changes/app/coffee/DiffGenerator.coffee
Normal file
|
@ -0,0 +1,168 @@
|
|||
ConsistencyError = (message) ->
|
||||
error = new Error(message)
|
||||
error.name = "ConsistencyError"
|
||||
error.__proto__ = ConsistencyError.prototype
|
||||
return error
|
||||
ConsistencyError.prototype.__proto__ = Error.prototype
|
||||
|
||||
module.exports = DiffGenerator =
|
||||
ConsistencyError: ConsistencyError
|
||||
|
||||
rewindUpdate: (content, update) ->
|
||||
op = update.op
|
||||
if op.i?
|
||||
textToBeRemoved = content.slice(op.p, op.p + op.i.length)
|
||||
if op.i != textToBeRemoved
|
||||
throw new ConsistencyError(
|
||||
"Inserted content, '#{op.i}', does not match text to be removed, '#{textToBeRemoved}'"
|
||||
)
|
||||
|
||||
return content.slice(0, op.p) + content.slice(op.p + op.i.length)
|
||||
|
||||
else if op.d?
|
||||
return content.slice(0, op.p) + op.d + content.slice(op.p)
|
||||
|
||||
rewindUpdates: (content, updates) ->
|
||||
for update in updates.reverse()
|
||||
content = DiffGenerator.rewindUpdate(content, update)
|
||||
return content
|
||||
|
||||
buildDiff: (initialContent, updates) ->
|
||||
|
||||
applyUpdateToDiff: (diff, update) ->
|
||||
position = 0
|
||||
op = update.op
|
||||
|
||||
remainingDiff = diff.slice()
|
||||
{consumedDiff, remainingDiff} = DiffGenerator._consumeToOffset(remainingDiff, op.p)
|
||||
newDiff = consumedDiff
|
||||
|
||||
if op.i?
|
||||
newDiff.push
|
||||
i: op.i
|
||||
meta: update.meta
|
||||
else if op.d?
|
||||
{consumedDiff, remainingDiff} = DiffGenerator._consumeDiffAffectedByDeleteUpdate remainingDiff, update
|
||||
newDiff.push(consumedDiff...)
|
||||
|
||||
newDiff.push(remainingDiff...)
|
||||
|
||||
return newDiff
|
||||
|
||||
|
||||
_consumeToOffset: (remainingDiff, totalOffset) ->
|
||||
consumedDiff = []
|
||||
position = 0
|
||||
while part = remainingDiff.shift()
|
||||
length = DiffGenerator._getLengthOfDiffPart part
|
||||
if part.d?
|
||||
consumedDiff.push part
|
||||
else if position + length >= totalOffset
|
||||
partOffset = totalOffset - position
|
||||
if partOffset > 0
|
||||
consumedDiff.push DiffGenerator._slicePart part, 0, partOffset
|
||||
if partOffset < length
|
||||
remainingDiff.unshift DiffGenerator._slicePart part, partOffset
|
||||
return {
|
||||
consumedDiff: consumedDiff
|
||||
remainingDiff: remainingDiff
|
||||
}
|
||||
else
|
||||
position += length
|
||||
consumedDiff.push part
|
||||
throw new Error("Ran out of diff to consume. Offset is too small")
|
||||
|
||||
_consumeDiffAffectedByDeleteUpdate: (remainingDiff, deleteUpdate) ->
|
||||
consumedDiff = []
|
||||
remainingUpdate = deleteUpdate
|
||||
while remainingUpdate
|
||||
{newPart, remainingDiff, remainingUpdate} = DiffGenerator._consumeDeletedPart remainingDiff, remainingUpdate
|
||||
consumedDiff.push newPart if newPart?
|
||||
return {
|
||||
consumedDiff: consumedDiff
|
||||
remainingDiff: remainingDiff
|
||||
}
|
||||
|
||||
_consumeDeletedPart: (remainingDiff, deleteUpdate) ->
|
||||
part = remainingDiff.shift()
|
||||
partLength = DiffGenerator._getLengthOfDiffPart part
|
||||
op = deleteUpdate.op
|
||||
|
||||
if part.d?
|
||||
# Skip existing deletes
|
||||
remainingUpdate = deleteUpdate
|
||||
newPart = part
|
||||
|
||||
else if partLength > op.d.length
|
||||
# Only the first bit of the part has been deleted
|
||||
remainingPart = DiffGenerator._slicePart part, op.d.length
|
||||
remainingDiff.unshift remainingPart
|
||||
|
||||
deletedContent = DiffGenerator._getContentOfPart(part).slice(0, op.d.length)
|
||||
if deletedContent != op.d
|
||||
throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
|
||||
|
||||
if part.u?
|
||||
newPart =
|
||||
d: op.d
|
||||
meta: deleteUpdate.meta
|
||||
else if part.i?
|
||||
newPart = null
|
||||
|
||||
remainingUpdate = null
|
||||
|
||||
else if partLength == op.d.length
|
||||
# The entire part has been deleted, but it is the last part
|
||||
|
||||
deletedContent = DiffGenerator._getContentOfPart(part)
|
||||
if deletedContent != op.d
|
||||
throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
|
||||
|
||||
if part.u?
|
||||
newPart =
|
||||
d: op.d
|
||||
meta: deleteUpdate.meta
|
||||
else if part.i?
|
||||
newPart = null
|
||||
|
||||
remainingUpdate = null
|
||||
|
||||
else if partLength < op.d.length
|
||||
# The entire part has been deleted and there is more
|
||||
|
||||
deletedContent = DiffGenerator._getContentOfPart(part)
|
||||
opContent = op.d.slice(0, deletedContent.length)
|
||||
if deletedContent != opContent
|
||||
throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{opContent}'")
|
||||
|
||||
if part.u
|
||||
newPart =
|
||||
d: part.u
|
||||
meta: deleteUpdate.meta
|
||||
else if part.i?
|
||||
newPart = null
|
||||
|
||||
remainingUpdate =
|
||||
op: { p: op.p, d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part)) }
|
||||
meta: deleteUpdate.meta
|
||||
|
||||
return {
|
||||
newPart: newPart
|
||||
remainingDiff: remainingDiff
|
||||
remainingUpdate: remainingUpdate
|
||||
}
|
||||
|
||||
_slicePart: (basePart, from, to) ->
|
||||
if basePart.u?
|
||||
part = { u: basePart.u.slice(from, to) }
|
||||
else if basePart.i?
|
||||
part = { i: basePart.i.slice(from, to) }
|
||||
if basePart.meta?
|
||||
part.meta = basePart.meta
|
||||
return part
|
||||
|
||||
_getLengthOfDiffPart: (part) ->
|
||||
(part.u or part.d or part.i).length
|
||||
|
||||
_getContentOfPart: (part) ->
|
||||
part.u or part.d or part.i
|
|
@ -0,0 +1,238 @@
|
|||
sinon = require('sinon')
|
||||
chai = require('chai')
|
||||
should = chai.should()
|
||||
expect = chai.expect
|
||||
modulePath = "../../../../app/js/DiffGenerator.js"
|
||||
SandboxedModule = require('sandboxed-module')
|
||||
|
||||
describe "DiffGenerator", ->
|
||||
beforeEach ->
|
||||
@DiffGenerator = SandboxedModule.require modulePath
|
||||
@ts = Date.now()
|
||||
@user_id = "mock-user-id"
|
||||
@meta = {
|
||||
start_ts: @ts, end_ts: @ts, user_id: @user_id
|
||||
}
|
||||
|
||||
describe "rewindUpdate", ->
|
||||
describe "rewinding an insert", ->
|
||||
it "should undo the insert", ->
|
||||
content = "hello world"
|
||||
update =
|
||||
op: { p: 6, i: "wo" }
|
||||
rewoundContent = @DiffGenerator.rewindUpdate content, update
|
||||
rewoundContent.should.equal "hello rld"
|
||||
|
||||
describe "rewinding a delete", ->
|
||||
it "should undo the delete", ->
|
||||
content = "hello rld"
|
||||
update =
|
||||
op: { p: 6, d: "wo" }
|
||||
rewoundContent = @DiffGenerator.rewindUpdate content, update
|
||||
rewoundContent.should.equal "hello world"
|
||||
|
||||
describe "with an inconsistent update", ->
|
||||
it "should throw an error", ->
|
||||
content = "hello world"
|
||||
update =
|
||||
op: { p: 6, i: "foo" }
|
||||
expect( () =>
|
||||
@DiffGenerator.rewindUpdate content, update
|
||||
).to.throw(@DiffGenerator.ConsistencyError)
|
||||
|
||||
describe "rewindUpdates", ->
|
||||
it "should rewind updates in reverse", ->
|
||||
content = "aaabbbccc"
|
||||
updates = [
|
||||
{ op: { p: 3, i: "bbb" } },
|
||||
{ op: { p: 6, i: "ccc" } }
|
||||
]
|
||||
rewoundContent = @DiffGenerator.rewindUpdates content, updates
|
||||
rewoundContent.should.equal "aaa"
|
||||
|
||||
describe "applyUpdateToDiff", ->
|
||||
describe "an insert", ->
|
||||
it "should insert into the middle of (u)nchanged text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobar" } ],
|
||||
{ op: { p: 3, i: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "foo" }
|
||||
{ i: "baz", meta: @meta }
|
||||
{ u: "bar" }
|
||||
])
|
||||
|
||||
it "should insert into the start of (u)changed text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobar" } ],
|
||||
{ op: { p: 0, i: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: "baz", meta: @meta }
|
||||
{ u: "foobar" }
|
||||
])
|
||||
|
||||
it "should insert into the end of (u)changed text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobar" } ],
|
||||
{ op: { p: 6, i: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "foobar" }
|
||||
{ i: "baz", meta: @meta }
|
||||
])
|
||||
|
||||
it "should insert into the middle of (i)inserted text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { i: "foobar", meta: @meta } ],
|
||||
{ op: { p: 3, i: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: "foo", meta: @meta }
|
||||
{ i: "baz", meta: @meta }
|
||||
{ i: "bar", meta: @meta }
|
||||
])
|
||||
|
||||
it "should not count deletes in the running length total", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[
|
||||
{ d: "deleted", meta: @meta }
|
||||
{ u: "foobar" }
|
||||
],
|
||||
{ op: { p: 3, i: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ d: "deleted", meta: @meta }
|
||||
{ u: "foo" }
|
||||
{ i: "baz", meta: @meta }
|
||||
{ u: "bar" }
|
||||
])
|
||||
|
||||
describe "a delete", ->
|
||||
describe "deleting unchanged text", ->
|
||||
it "should delete from the middle of (u)nchanged text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobazbar" } ],
|
||||
{ op: { p: 3, d: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "foo" }
|
||||
{ d: "baz", meta: @meta }
|
||||
{ u: "bar" }
|
||||
])
|
||||
|
||||
it "should delete from the start of (u)nchanged text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobazbar" } ],
|
||||
{ op: { p: 0, d: "foo" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ d: "foo", meta: @meta }
|
||||
{ u: "bazbar" }
|
||||
])
|
||||
|
||||
it "should delete from the end of (u)nchanged text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobazbar" } ],
|
||||
{ op: { p: 6, d: "bar" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "foobaz" }
|
||||
{ d: "bar", meta: @meta }
|
||||
])
|
||||
|
||||
it "should delete across multiple (u)changed text parts", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foo" }, { u: "baz" }, { u: "bar" } ],
|
||||
{ op: { p: 2, d: "obazb" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "fo" }
|
||||
{ d: "o", meta: @meta }
|
||||
{ d: "baz", meta: @meta }
|
||||
{ d: "b", meta: @meta }
|
||||
{ u: "ar" }
|
||||
])
|
||||
|
||||
describe "deleting inserts", ->
|
||||
it "should delete from the middle of (i)nserted text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { i: "foobazbar", meta: @meta } ],
|
||||
{ op: { p: 3, d: "baz" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: "foo", meta: @meta }
|
||||
{ i: "bar", meta: @meta }
|
||||
])
|
||||
|
||||
it "should delete from the start of (u)nchanged text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { i: "foobazbar", meta: @meta } ],
|
||||
{ op: { p: 0, d: "foo" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: "bazbar", meta: @meta }
|
||||
])
|
||||
|
||||
it "should delete from the end of (u)nchanged text", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { i: "foobazbar", meta: @meta } ],
|
||||
{ op: { p: 6, d: "bar" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ i: "foobaz", meta: @meta }
|
||||
])
|
||||
|
||||
it "should delete across multiple (u)changed and (i)nserted text parts", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foo" }, { i: "baz", meta: @meta }, { u: "bar" } ],
|
||||
{ op: { p: 2, d: "obazb" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "fo" }
|
||||
{ d: "o", meta: @meta }
|
||||
{ d: "b", meta: @meta }
|
||||
{ u: "ar" }
|
||||
])
|
||||
|
||||
describe "deleting over existing deletes", ->
|
||||
it "should delete across multiple (u)changed and (d)deleted text parts", ->
|
||||
diff = @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foo" }, { d: "baz", meta: @meta }, { u: "bar" } ],
|
||||
{ op: { p: 2, d: "ob" }, meta: @meta }
|
||||
)
|
||||
expect(diff).to.deep.equal([
|
||||
{ u: "fo" }
|
||||
{ d: "o", meta: @meta }
|
||||
{ d: "baz", meta: @meta }
|
||||
{ d: "b", meta: @meta }
|
||||
{ u: "ar" }
|
||||
])
|
||||
|
||||
describe "deleting when the text doesn't match", ->
|
||||
it "should throw an error when deleting from the middle of (u)nchanged text", ->
|
||||
expect(
|
||||
() => @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobazbar" } ],
|
||||
{ op: { p: 3, d: "xxx" }, meta: @meta }
|
||||
)
|
||||
).to.throw(@DiffGenerator.ConsistencyError)
|
||||
|
||||
it "should throw an error when deleting from the start of (u)nchanged text", ->
|
||||
expect(
|
||||
() => @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobazbar" } ],
|
||||
{ op: { p: 0, d: "xxx" }, meta: @meta }
|
||||
)
|
||||
).to.throw(@DiffGenerator.ConsistencyError)
|
||||
|
||||
it "should throw an error when deleting from the end of (u)nchanged text", ->
|
||||
expect(
|
||||
() => @DiffGenerator.applyUpdateToDiff(
|
||||
[ { u: "foobazbar" } ],
|
||||
{ op: { p: 6, d: "xxx" }, meta: @meta }
|
||||
)
|
||||
).to.throw(@DiffGenerator.ConsistencyError)
|
||||
|
||||
|
Loading…
Reference in a new issue