DiffGenerator.coffee 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. diff = DiffGenerator.compressDiff diff
  32. return diff
  33. compressDiff: (diff) ->
  34. newDiff = []
  35. for part in diff
  36. lastPart = newDiff[newDiff.length - 1]
  37. if lastPart? and lastPart.meta?.user? and part.meta?.user?
  38. if lastPart.i? and part.i? and lastPart.meta.user.id == part.meta.user.id
  39. lastPart.i += part.i
  40. lastPart.meta.start_ts = Math.min(lastPart.meta.start_ts, part.meta.start_ts)
  41. lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
  42. else if lastPart.d? and part.d? and lastPart.meta.user.id == part.meta.user.id
  43. lastPart.d += part.d
  44. lastPart.meta.start_ts = Math.min(lastPart.meta.start_ts, part.meta.start_ts)
  45. lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
  46. else
  47. newDiff.push part
  48. else
  49. newDiff.push part
  50. return newDiff
  51. applyOpToDiff: (diff, op, meta) ->
  52. position = 0
  53. remainingDiff = diff.slice()
  54. {consumedDiff, remainingDiff} = DiffGenerator._consumeToOffset(remainingDiff, op.p)
  55. newDiff = consumedDiff
  56. if op.i?
  57. newDiff.push
  58. i: op.i
  59. meta: meta
  60. else if op.d?
  61. {consumedDiff, remainingDiff} = DiffGenerator._consumeDiffAffectedByDeleteOp remainingDiff, op, meta
  62. newDiff.push(consumedDiff...)
  63. newDiff.push(remainingDiff...)
  64. return newDiff
  65. applyUpdateToDiff: (diff, update) ->
  66. for op in update.op
  67. diff = DiffGenerator.applyOpToDiff diff, op, update.meta
  68. return diff
  69. _consumeToOffset: (remainingDiff, totalOffset) ->
  70. consumedDiff = []
  71. position = 0
  72. while part = remainingDiff.shift()
  73. length = DiffGenerator._getLengthOfDiffPart part
  74. if part.d?
  75. consumedDiff.push part
  76. else if position + length >= totalOffset
  77. partOffset = totalOffset - position
  78. if partOffset > 0
  79. consumedDiff.push DiffGenerator._slicePart part, 0, partOffset
  80. if partOffset < length
  81. remainingDiff.unshift DiffGenerator._slicePart part, partOffset
  82. break
  83. else
  84. position += length
  85. consumedDiff.push part
  86. return {
  87. consumedDiff: consumedDiff
  88. remainingDiff: remainingDiff
  89. }
  90. _consumeDiffAffectedByDeleteOp: (remainingDiff, deleteOp, meta) ->
  91. consumedDiff = []
  92. remainingOp = deleteOp
  93. while remainingOp and remainingDiff.length > 0
  94. {newPart, remainingDiff, remainingOp} = DiffGenerator._consumeDeletedPart remainingDiff, remainingOp, meta
  95. consumedDiff.push newPart if newPart?
  96. return {
  97. consumedDiff: consumedDiff
  98. remainingDiff: remainingDiff
  99. }
  100. _consumeDeletedPart: (remainingDiff, op, meta) ->
  101. part = remainingDiff.shift()
  102. partLength = DiffGenerator._getLengthOfDiffPart part
  103. if part.d?
  104. # Skip existing deletes
  105. remainingOp = op
  106. newPart = part
  107. else if partLength > op.d.length
  108. # Only the first bit of the part has been deleted
  109. remainingPart = DiffGenerator._slicePart part, op.d.length
  110. remainingDiff.unshift remainingPart
  111. deletedContent = DiffGenerator._getContentOfPart(part).slice(0, op.d.length)
  112. if deletedContent != op.d
  113. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
  114. if part.u?
  115. newPart =
  116. d: op.d
  117. meta: meta
  118. else if part.i?
  119. newPart = null
  120. remainingOp = null
  121. else if partLength == op.d.length
  122. # The entire part has been deleted, but it is the last part
  123. deletedContent = DiffGenerator._getContentOfPart(part)
  124. if deletedContent != op.d
  125. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
  126. if part.u?
  127. newPart =
  128. d: op.d
  129. meta: meta
  130. else if part.i?
  131. newPart = null
  132. remainingOp = null
  133. else if partLength < op.d.length
  134. # The entire part has been deleted and there is more
  135. deletedContent = DiffGenerator._getContentOfPart(part)
  136. opContent = op.d.slice(0, deletedContent.length)
  137. if deletedContent != opContent
  138. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{opContent}'")
  139. if part.u
  140. newPart =
  141. d: part.u
  142. meta: meta
  143. else if part.i?
  144. newPart = null
  145. remainingOp =
  146. p: op.p, d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part))
  147. return {
  148. newPart: newPart
  149. remainingDiff: remainingDiff
  150. remainingOp: remainingOp
  151. }
  152. _slicePart: (basePart, from, to) ->
  153. if basePart.u?
  154. part = { u: basePart.u.slice(from, to) }
  155. else if basePart.i?
  156. part = { i: basePart.i.slice(from, to) }
  157. if basePart.meta?
  158. part.meta = basePart.meta
  159. return part
  160. _getLengthOfDiffPart: (part) ->
  161. (part.u or part.d or part.i or '').length
  162. _getContentOfPart: (part) ->
  163. part.u or part.d or part.i or ''