DiffGenerator.coffee 4.8 KB

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