DiffGenerator.coffee 6.6 KB

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