HistoryBlobTranslator.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. // @ts-check
  2. import {
  3. Range,
  4. TrackedChange,
  5. TrackedChangeList,
  6. CommentList,
  7. Comment,
  8. TrackingProps,
  9. } from 'overleaf-editor-core'
  10. import logger from '@overleaf/logger'
  11. import OError from '@overleaf/o-error'
  12. /**
  13. * @import { AddDocUpdate } from './types'
  14. * @import { CommentRawData, TrackedChangeRawData } from 'overleaf-editor-core/lib/types'
  15. */
  16. /**
  17. *
  18. * @param {AddDocUpdate} update
  19. * @returns {{trackedChanges: TrackedChangeRawData[], comments: CommentRawData[]} | undefined}
  20. */
  21. export function createRangeBlobDataFromUpdate(update) {
  22. logger.debug({ update }, 'createBlobDataFromUpdate')
  23. if (update.doc == null || update.docLines == null) {
  24. throw new OError('Not an AddFileUpdate')
  25. }
  26. if (
  27. !update.ranges ||
  28. (update.ranges.changes == null && update.ranges.comments == null)
  29. ) {
  30. return undefined
  31. }
  32. if (
  33. (!update.ranges.changes || update.ranges.changes.length === 0) &&
  34. (!update.ranges.comments || update.ranges.comments.length === 0)
  35. ) {
  36. return undefined
  37. }
  38. const sortedRanges = [...(update.ranges.changes || [])].sort((a, b) => {
  39. if (a.op.p !== b.op.p) {
  40. return a.op.p - b.op.p
  41. }
  42. if ('i' in a.op && a.op.i != null && 'd' in b.op && b.op.d != null) {
  43. // Move deletes before inserts
  44. return 1
  45. }
  46. return -1
  47. })
  48. const tcList = new TrackedChangeList([])
  49. for (const change of sortedRanges) {
  50. if ('d' in change.op && change.op.d != null) {
  51. const length = change.op.d.length
  52. const range = new Range(change.op.hpos ?? change.op.p, length)
  53. tcList.add(
  54. new TrackedChange(
  55. range,
  56. new TrackingProps(
  57. 'delete',
  58. change.metadata.user_id,
  59. new Date(change.metadata.ts)
  60. )
  61. )
  62. )
  63. } else if ('i' in change.op && change.op.i != null) {
  64. const length = change.op.i.length
  65. const range = new Range(change.op.hpos ?? change.op.p, length)
  66. tcList.add(
  67. new TrackedChange(
  68. range,
  69. new TrackingProps(
  70. 'insert',
  71. change.metadata.user_id,
  72. new Date(change.metadata.ts)
  73. )
  74. )
  75. )
  76. }
  77. }
  78. const comments = [...(update.ranges.comments || [])].sort((a, b) => {
  79. return a.op.p - b.op.p
  80. })
  81. /** @type {Map<string, {ranges: Range[], resolved: boolean}>} */
  82. const commentMap = new Map()
  83. for (const comment of comments) {
  84. const id = comment.op.t
  85. if (!commentMap.has(id)) {
  86. commentMap.set(id, {
  87. ranges: [],
  88. resolved: comment.op.resolved ?? false,
  89. })
  90. }
  91. const entry = commentMap.get(id)
  92. if (!entry) {
  93. throw new Error('Comment entry not found')
  94. }
  95. if (entry.resolved !== (comment.op.resolved ?? false)) {
  96. throw new Error('Mismatching resolved status for comment')
  97. }
  98. const commentLength = comment.op.c.length
  99. if (commentLength > 0) {
  100. // Empty comments in operations are translated to detached comments
  101. const range = new Range(comment.op.hpos ?? comment.op.p, commentLength)
  102. entry.ranges.push(range)
  103. }
  104. }
  105. const commentList = new CommentList(
  106. [...commentMap.entries()].map(
  107. ([id, commentObj]) =>
  108. new Comment(id, commentObj.ranges, commentObj.resolved)
  109. )
  110. )
  111. return { trackedChanges: tcList.toRaw(), comments: commentList.toRaw() }
  112. }