DiffGenerator.coffee 4.8 KB

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