DiffCodec.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. const OError = require('@overleaf/o-error')
  2. const DMP = require('diff-match-patch')
  3. const { TextOperation } = require('overleaf-editor-core')
  4. const dmp = new DMP()
  5. // Do not attempt to produce a diff for more than 100ms
  6. dmp.Diff_Timeout = 0.1
  7. module.exports = {
  8. ADDED: 1,
  9. REMOVED: -1,
  10. UNCHANGED: 0,
  11. diffAsShareJsOp(before, after) {
  12. const diffs = dmp.diff_main(before.join('\n'), after.join('\n'))
  13. dmp.diff_cleanupSemantic(diffs)
  14. const ops = []
  15. let position = 0
  16. for (const diff of diffs) {
  17. const [type, content] = diff
  18. if (type === this.ADDED) {
  19. ops.push({
  20. i: content,
  21. p: position,
  22. })
  23. position += content.length
  24. } else if (type === this.REMOVED) {
  25. ops.push({
  26. d: content,
  27. p: position,
  28. })
  29. } else if (type === this.UNCHANGED) {
  30. position += content.length
  31. } else {
  32. throw new Error('Unknown type')
  33. }
  34. }
  35. return ops
  36. },
  37. /**
  38. * @param {import("overleaf-editor-core").StringFileData} file
  39. * @param {string} after
  40. * @return {TextOperation}
  41. */
  42. diffAsHistoryOTEditOperation(file, after) {
  43. const beforeWithoutTrackedDeletes = file.getContent({
  44. filterTrackedDeletes: true,
  45. })
  46. const diffs = dmp.diff_main(beforeWithoutTrackedDeletes, after)
  47. dmp.diff_cleanupSemantic(diffs)
  48. const trackedChanges = file.trackedChanges.asSorted()
  49. let nextTc = trackedChanges.shift()
  50. const op = new TextOperation()
  51. for (const diff of diffs) {
  52. let [type, content] = diff
  53. if (type === this.ADDED) {
  54. op.insert(content)
  55. } else if (type === this.REMOVED || type === this.UNCHANGED) {
  56. while (op.baseLength + content.length > nextTc?.range.start) {
  57. if (nextTc.tracking.type === 'delete') {
  58. const untilRange = nextTc.range.start - op.baseLength
  59. if (type === this.REMOVED) {
  60. op.remove(untilRange)
  61. } else if (type === this.UNCHANGED) {
  62. op.retain(untilRange)
  63. }
  64. op.retain(nextTc.range.end - nextTc.range.start)
  65. content = content.slice(untilRange)
  66. }
  67. nextTc = trackedChanges.shift()
  68. }
  69. if (type === this.REMOVED) {
  70. op.remove(content.length)
  71. } else if (type === this.UNCHANGED) {
  72. op.retain(content.length)
  73. }
  74. } else {
  75. throw new Error('Unknown type')
  76. }
  77. }
  78. while (nextTc) {
  79. if (
  80. nextTc.tracking.type !== 'delete' ||
  81. nextTc.range.start !== op.baseLength
  82. ) {
  83. throw new OError(
  84. 'StringFileData.trackedChanges out of sync: unexpected range after end of diff',
  85. { nextTc, baseLength: op.baseLength }
  86. )
  87. }
  88. op.retain(nextTc.range.end - nextTc.range.start)
  89. nextTc = trackedChanges.shift()
  90. }
  91. return op
  92. },
  93. }