DiffGenerator.coffee 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. ConsistencyError = (message) ->
  2. error = new Error(message)
  3. error.name = "ConsistencyError"
  4. error.__proto__ = ConsistencyError.prototype
  5. return error
  6. ConsistencyError.prototype.__proto__ = Error.prototype
  7. module.exports = DiffGenerator =
  8. ConsistencyError: ConsistencyError
  9. rewindUpdate: (content, update) ->
  10. op = update.op
  11. if op.i?
  12. textToBeRemoved = content.slice(op.p, op.p + op.i.length)
  13. if op.i != textToBeRemoved
  14. throw new ConsistencyError(
  15. "Inserted content, '#{op.i}', does not match text to be removed, '#{textToBeRemoved}'"
  16. )
  17. return content.slice(0, op.p) + content.slice(op.p + op.i.length)
  18. else if op.d?
  19. return content.slice(0, op.p) + op.d + content.slice(op.p)
  20. rewindUpdates: (content, updates) ->
  21. for update in updates.reverse()
  22. content = DiffGenerator.rewindUpdate(content, update)
  23. return content
  24. buildDiff: (initialContent, updates) ->
  25. applyUpdateToDiff: (diff, update) ->
  26. position = 0
  27. op = update.op
  28. remainingDiff = diff.slice()
  29. {consumedDiff, remainingDiff} = DiffGenerator._consumeToOffset(remainingDiff, op.p)
  30. newDiff = consumedDiff
  31. if op.i?
  32. newDiff.push
  33. i: op.i
  34. meta: update.meta
  35. else if op.d?
  36. {consumedDiff, remainingDiff} = DiffGenerator._consumeDiffAffectedByDeleteUpdate remainingDiff, update
  37. newDiff.push(consumedDiff...)
  38. newDiff.push(remainingDiff...)
  39. return newDiff
  40. _consumeToOffset: (remainingDiff, totalOffset) ->
  41. consumedDiff = []
  42. position = 0
  43. while part = remainingDiff.shift()
  44. length = DiffGenerator._getLengthOfDiffPart part
  45. if part.d?
  46. consumedDiff.push part
  47. else if position + length >= totalOffset
  48. partOffset = totalOffset - position
  49. if partOffset > 0
  50. consumedDiff.push DiffGenerator._slicePart part, 0, partOffset
  51. if partOffset < length
  52. remainingDiff.unshift DiffGenerator._slicePart part, partOffset
  53. return {
  54. consumedDiff: consumedDiff
  55. remainingDiff: remainingDiff
  56. }
  57. else
  58. position += length
  59. consumedDiff.push part
  60. throw new Error("Ran out of diff to consume. Offset is too small")
  61. _consumeDiffAffectedByDeleteUpdate: (remainingDiff, deleteUpdate) ->
  62. consumedDiff = []
  63. remainingUpdate = deleteUpdate
  64. while remainingUpdate
  65. {newPart, remainingDiff, remainingUpdate} = DiffGenerator._consumeDeletedPart remainingDiff, remainingUpdate
  66. consumedDiff.push newPart if newPart?
  67. return {
  68. consumedDiff: consumedDiff
  69. remainingDiff: remainingDiff
  70. }
  71. _consumeDeletedPart: (remainingDiff, deleteUpdate) ->
  72. part = remainingDiff.shift()
  73. partLength = DiffGenerator._getLengthOfDiffPart part
  74. op = deleteUpdate.op
  75. if part.d?
  76. # Skip existing deletes
  77. remainingUpdate = deleteUpdate
  78. newPart = part
  79. else if partLength > op.d.length
  80. # Only the first bit of the part has been deleted
  81. remainingPart = DiffGenerator._slicePart part, op.d.length
  82. remainingDiff.unshift remainingPart
  83. deletedContent = DiffGenerator._getContentOfPart(part).slice(0, op.d.length)
  84. if deletedContent != op.d
  85. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
  86. if part.u?
  87. newPart =
  88. d: op.d
  89. meta: deleteUpdate.meta
  90. else if part.i?
  91. newPart = null
  92. remainingUpdate = null
  93. else if partLength == op.d.length
  94. # The entire part has been deleted, but it is the last part
  95. deletedContent = DiffGenerator._getContentOfPart(part)
  96. if deletedContent != op.d
  97. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
  98. if part.u?
  99. newPart =
  100. d: op.d
  101. meta: deleteUpdate.meta
  102. else if part.i?
  103. newPart = null
  104. remainingUpdate = null
  105. else if partLength < op.d.length
  106. # The entire part has been deleted and there is more
  107. deletedContent = DiffGenerator._getContentOfPart(part)
  108. opContent = op.d.slice(0, deletedContent.length)
  109. if deletedContent != opContent
  110. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{opContent}'")
  111. if part.u
  112. newPart =
  113. d: part.u
  114. meta: deleteUpdate.meta
  115. else if part.i?
  116. newPart = null
  117. remainingUpdate =
  118. op: { p: op.p, d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part)) }
  119. meta: deleteUpdate.meta
  120. return {
  121. newPart: newPart
  122. remainingDiff: remainingDiff
  123. remainingUpdate: remainingUpdate
  124. }
  125. _slicePart: (basePart, from, to) ->
  126. if basePart.u?
  127. part = { u: basePart.u.slice(from, to) }
  128. else if basePart.i?
  129. part = { i: basePart.i.slice(from, to) }
  130. if basePart.meta?
  131. part.meta = basePart.meta
  132. return part
  133. _getLengthOfDiffPart: (part) ->
  134. (part.u or part.d or part.i).length
  135. _getContentOfPart: (part) ->
  136. part.u or part.d or part.i