DiffGenerator.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. /* eslint-disable
  2. no-proto,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. import logger from '@overleaf/logger'
  15. import OError from '@overleaf/o-error'
  16. export class ConsistencyError extends OError {}
  17. /**
  18. * Container for functions that need to be mocked in tests
  19. *
  20. * TODO: Rewrite tests in terms of exported functions only
  21. */
  22. export const _mocks = {}
  23. export function buildDiff(initialContent, updates) {
  24. let diff = [{ u: initialContent }]
  25. for (const update of Array.from(updates)) {
  26. diff = applyUpdateToDiff(diff, update)
  27. }
  28. diff = compressDiff(diff)
  29. return diff
  30. }
  31. _mocks.compressDiff = diff => {
  32. const newDiff = []
  33. for (const part of Array.from(diff)) {
  34. const lastPart = newDiff[newDiff.length - 1]
  35. if (
  36. lastPart != null &&
  37. (lastPart.meta != null ? lastPart.meta.user : undefined) != null &&
  38. (part.meta != null ? part.meta.user : undefined) != null
  39. ) {
  40. if (
  41. lastPart.i != null &&
  42. part.i != null &&
  43. lastPart.meta.user.id === part.meta.user.id
  44. ) {
  45. lastPart.i += part.i
  46. lastPart.meta.start_ts = Math.min(
  47. lastPart.meta.start_ts,
  48. part.meta.start_ts
  49. )
  50. lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
  51. } else if (
  52. lastPart.d != null &&
  53. part.d != null &&
  54. lastPart.meta.user.id === part.meta.user.id
  55. ) {
  56. lastPart.d += part.d
  57. lastPart.meta.start_ts = Math.min(
  58. lastPart.meta.start_ts,
  59. part.meta.start_ts
  60. )
  61. lastPart.meta.end_ts = Math.max(lastPart.meta.end_ts, part.meta.end_ts)
  62. } else {
  63. newDiff.push(part)
  64. }
  65. } else {
  66. newDiff.push(part)
  67. }
  68. }
  69. return newDiff
  70. }
  71. export function compressDiff(...args) {
  72. return _mocks.compressDiff(...args)
  73. }
  74. export function applyOpToDiff(diff, op, meta) {
  75. let consumedDiff
  76. const position = 0
  77. let remainingDiff = diff.slice()
  78. ;({ consumedDiff, remainingDiff } = _consumeToOffset(remainingDiff, op.p))
  79. const newDiff = consumedDiff
  80. if (op.i != null) {
  81. newDiff.push({
  82. i: op.i,
  83. meta,
  84. })
  85. } else if (op.d != null) {
  86. ;({ consumedDiff, remainingDiff } = _consumeDiffAffectedByDeleteOp(
  87. remainingDiff,
  88. op,
  89. meta
  90. ))
  91. newDiff.push(...Array.from(consumedDiff || []))
  92. }
  93. newDiff.push(...Array.from(remainingDiff || []))
  94. return newDiff
  95. }
  96. _mocks.applyUpdateToDiff = (diff, update) => {
  97. for (const op of Array.from(update.op)) {
  98. if (op.broken !== true) {
  99. diff = applyOpToDiff(diff, op, update.meta)
  100. }
  101. }
  102. return diff
  103. }
  104. export function applyUpdateToDiff(...args) {
  105. return _mocks.applyUpdateToDiff(...args)
  106. }
  107. function _consumeToOffset(remainingDiff, totalOffset) {
  108. let part
  109. const consumedDiff = []
  110. let position = 0
  111. while ((part = remainingDiff.shift())) {
  112. const length = _getLengthOfDiffPart(part)
  113. if (part.d != null) {
  114. consumedDiff.push(part)
  115. } else if (position + length >= totalOffset) {
  116. const partOffset = totalOffset - position
  117. if (partOffset > 0) {
  118. consumedDiff.push(_slicePart(part, 0, partOffset))
  119. }
  120. if (partOffset < length) {
  121. remainingDiff.unshift(_slicePart(part, partOffset))
  122. }
  123. break
  124. } else {
  125. position += length
  126. consumedDiff.push(part)
  127. }
  128. }
  129. return {
  130. consumedDiff,
  131. remainingDiff,
  132. }
  133. }
  134. function _consumeDiffAffectedByDeleteOp(remainingDiff, deleteOp, meta) {
  135. const consumedDiff = []
  136. let remainingOp = deleteOp
  137. while (remainingOp && remainingDiff.length > 0) {
  138. let newPart
  139. ;({ newPart, remainingDiff, remainingOp } = _consumeDeletedPart(
  140. remainingDiff,
  141. remainingOp,
  142. meta
  143. ))
  144. if (newPart != null) {
  145. consumedDiff.push(newPart)
  146. }
  147. }
  148. return {
  149. consumedDiff,
  150. remainingDiff,
  151. }
  152. }
  153. function _consumeDeletedPart(remainingDiff, op, meta) {
  154. let deletedContent, newPart, remainingOp
  155. const part = remainingDiff.shift()
  156. const partLength = _getLengthOfDiffPart(part)
  157. if (part.d != null) {
  158. // Skip existing deletes
  159. remainingOp = op
  160. newPart = part
  161. } else if (partLength > op.d.length) {
  162. // Only the first bit of the part has been deleted
  163. const remainingPart = _slicePart(part, op.d.length)
  164. remainingDiff.unshift(remainingPart)
  165. deletedContent = _getContentOfPart(part).slice(0, op.d.length)
  166. if (deletedContent !== op.d) {
  167. throw new ConsistencyError(
  168. `deleted content, '${deletedContent}', does not match delete op, '${op.d}'`
  169. )
  170. }
  171. if (part.u != null) {
  172. newPart = {
  173. d: op.d,
  174. meta,
  175. }
  176. } else if (part.i != null) {
  177. newPart = null
  178. }
  179. remainingOp = null
  180. } else if (partLength === op.d.length) {
  181. // The entire part has been deleted, but it is the last part
  182. deletedContent = _getContentOfPart(part)
  183. if (deletedContent !== op.d) {
  184. throw new ConsistencyError(
  185. `deleted content, '${deletedContent}', does not match delete op, '${op.d}'`
  186. )
  187. }
  188. if (part.u != null) {
  189. newPart = {
  190. d: op.d,
  191. meta,
  192. }
  193. } else if (part.i != null) {
  194. newPart = null
  195. }
  196. remainingOp = null
  197. } else if (partLength < op.d.length) {
  198. // The entire part has been deleted and there is more
  199. deletedContent = _getContentOfPart(part)
  200. const opContent = op.d.slice(0, deletedContent.length)
  201. if (deletedContent !== opContent) {
  202. throw new ConsistencyError(
  203. `deleted content, '${deletedContent}', does not match delete op, '${opContent}'`
  204. )
  205. }
  206. if (part.u) {
  207. newPart = {
  208. d: part.u,
  209. meta,
  210. }
  211. } else if (part.i != null) {
  212. newPart = null
  213. }
  214. remainingOp = {
  215. p: op.p,
  216. d: op.d.slice(_getLengthOfDiffPart(part)),
  217. }
  218. }
  219. return {
  220. newPart,
  221. remainingDiff,
  222. remainingOp,
  223. }
  224. }
  225. function _slicePart(basePart, from, to) {
  226. let part
  227. if (basePart.u != null) {
  228. part = { u: basePart.u.slice(from, to) }
  229. } else if (basePart.i != null) {
  230. part = { i: basePart.i.slice(from, to) }
  231. }
  232. if (basePart.meta != null) {
  233. part.meta = basePart.meta
  234. }
  235. return part
  236. }
  237. function _getLengthOfDiffPart(part) {
  238. return (part.u || part.d || part.i || '').length
  239. }
  240. function _getContentOfPart(part) {
  241. return part.u || part.d || part.i || ''
  242. }