DiffGenerator.coffee 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  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. logger = require "logger-sharelatex"
  8. module.exports = DiffGenerator =
  9. ConsistencyError: ConsistencyError
  10. rewindUpdate: (content, update) ->
  11. for op in update.op.slice().reverse()
  12. content = DiffGenerator.rewindOp content, op
  13. return content
  14. rewindOp: (content, op) ->
  15. if op.i?
  16. # ShareJS will accept an op where p > content.length when applied,
  17. # and it applies as though p == content.length. However, the op is
  18. # passed to us with the original p > content.length. Detect if that
  19. # is the case with this op, and shift p back appropriately to match
  20. # ShareJS if so.
  21. p = op.p
  22. max_p = content.length - op.i.length
  23. if p > max_p
  24. logger.warn {max_p, p}, "truncating position to content length"
  25. p = max_p
  26. textToBeRemoved = content.slice(p, p + op.i.length)
  27. if op.i != textToBeRemoved
  28. throw new ConsistencyError(
  29. "Inserted content, '#{op.i}', does not match text to be removed, '#{textToBeRemoved}'"
  30. )
  31. return content.slice(0, p) + content.slice(p + op.i.length)
  32. else if op.d?
  33. return content.slice(0, op.p) + op.d + content.slice(op.p)
  34. rewindUpdates: (content, updates) ->
  35. for update in updates.reverse()
  36. content = DiffGenerator.rewindUpdate(content, update)
  37. return content
  38. buildDiff: (initialContent, updates) ->
  39. diff = [ u: initialContent ]
  40. for update in updates
  41. diff = DiffGenerator.applyUpdateToDiff diff, update
  42. diff = DiffGenerator.compressDiff diff
  43. return diff
  44. compressDiff: (diff) ->
  45. newDiff = []
  46. for part in diff
  47. lastPart = newDiff[newDiff.length - 1]
  48. if lastPart? and lastPart.meta?.user? and part.meta?.user?
  49. if lastPart.i? and part.i? and lastPart.meta.user.id == part.meta.user.id
  50. lastPart.i += part.i
  51. lastPart.meta.start_ts = Math.min(lastPart.meta.start_ts, part.meta.start_ts)
  52. lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
  53. else if lastPart.d? and part.d? and lastPart.meta.user.id == part.meta.user.id
  54. lastPart.d += part.d
  55. lastPart.meta.start_ts = Math.min(lastPart.meta.start_ts, part.meta.start_ts)
  56. lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
  57. else
  58. newDiff.push part
  59. else
  60. newDiff.push part
  61. return newDiff
  62. applyOpToDiff: (diff, op, meta) ->
  63. position = 0
  64. remainingDiff = diff.slice()
  65. {consumedDiff, remainingDiff} = DiffGenerator._consumeToOffset(remainingDiff, op.p)
  66. newDiff = consumedDiff
  67. if op.i?
  68. newDiff.push
  69. i: op.i
  70. meta: meta
  71. else if op.d?
  72. {consumedDiff, remainingDiff} = DiffGenerator._consumeDiffAffectedByDeleteOp remainingDiff, op, meta
  73. newDiff.push(consumedDiff...)
  74. newDiff.push(remainingDiff...)
  75. return newDiff
  76. applyUpdateToDiff: (diff, update) ->
  77. for op in update.op
  78. diff = DiffGenerator.applyOpToDiff diff, op, update.meta
  79. return diff
  80. _consumeToOffset: (remainingDiff, totalOffset) ->
  81. consumedDiff = []
  82. position = 0
  83. while part = remainingDiff.shift()
  84. length = DiffGenerator._getLengthOfDiffPart part
  85. if part.d?
  86. consumedDiff.push part
  87. else if position + length >= totalOffset
  88. partOffset = totalOffset - position
  89. if partOffset > 0
  90. consumedDiff.push DiffGenerator._slicePart part, 0, partOffset
  91. if partOffset < length
  92. remainingDiff.unshift DiffGenerator._slicePart part, partOffset
  93. break
  94. else
  95. position += length
  96. consumedDiff.push part
  97. return {
  98. consumedDiff: consumedDiff
  99. remainingDiff: remainingDiff
  100. }
  101. _consumeDiffAffectedByDeleteOp: (remainingDiff, deleteOp, meta) ->
  102. consumedDiff = []
  103. remainingOp = deleteOp
  104. while remainingOp and remainingDiff.length > 0
  105. {newPart, remainingDiff, remainingOp} = DiffGenerator._consumeDeletedPart remainingDiff, remainingOp, meta
  106. consumedDiff.push newPart if newPart?
  107. return {
  108. consumedDiff: consumedDiff
  109. remainingDiff: remainingDiff
  110. }
  111. _consumeDeletedPart: (remainingDiff, op, meta) ->
  112. part = remainingDiff.shift()
  113. partLength = DiffGenerator._getLengthOfDiffPart part
  114. if part.d?
  115. # Skip existing deletes
  116. remainingOp = op
  117. newPart = part
  118. else if partLength > op.d.length
  119. # Only the first bit of the part has been deleted
  120. remainingPart = DiffGenerator._slicePart part, op.d.length
  121. remainingDiff.unshift remainingPart
  122. deletedContent = DiffGenerator._getContentOfPart(part).slice(0, op.d.length)
  123. if deletedContent != op.d
  124. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
  125. if part.u?
  126. newPart =
  127. d: op.d
  128. meta: meta
  129. else if part.i?
  130. newPart = null
  131. remainingOp = null
  132. else if partLength == op.d.length
  133. # The entire part has been deleted, but it is the last part
  134. deletedContent = DiffGenerator._getContentOfPart(part)
  135. if deletedContent != op.d
  136. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{op.d}'")
  137. if part.u?
  138. newPart =
  139. d: op.d
  140. meta: meta
  141. else if part.i?
  142. newPart = null
  143. remainingOp = null
  144. else if partLength < op.d.length
  145. # The entire part has been deleted and there is more
  146. deletedContent = DiffGenerator._getContentOfPart(part)
  147. opContent = op.d.slice(0, deletedContent.length)
  148. if deletedContent != opContent
  149. throw new ConsistencyError("deleted content, '#{deletedContent}', does not match delete op, '#{opContent}'")
  150. if part.u
  151. newPart =
  152. d: part.u
  153. meta: meta
  154. else if part.i?
  155. newPart = null
  156. remainingOp =
  157. p: op.p, d: op.d.slice(DiffGenerator._getLengthOfDiffPart(part))
  158. return {
  159. newPart: newPart
  160. remainingDiff: remainingDiff
  161. remainingOp: remainingOp
  162. }
  163. _slicePart: (basePart, from, to) ->
  164. if basePart.u?
  165. part = { u: basePart.u.slice(from, to) }
  166. else if basePart.i?
  167. part = { i: basePart.i.slice(from, to) }
  168. if basePart.meta?
  169. part.meta = basePart.meta
  170. return part
  171. _getLengthOfDiffPart: (part) ->
  172. (part.u or part.d or part.i or '').length
  173. _getContentOfPart: (part) ->
  174. part.u or part.d or part.i or ''