Utils.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. // @ts-check
  2. const { createHash } = require('node:crypto')
  3. const _ = require('lodash')
  4. /**
  5. * @import { CommentOp, DeleteOp, InsertOp, Op, TrackedChange } from './types'
  6. */
  7. /**
  8. * Returns true if the op is an insert
  9. *
  10. * @param {Op} op
  11. * @returns {op is InsertOp}
  12. */
  13. function isInsert(op) {
  14. return 'i' in op && op.i != null
  15. }
  16. /**
  17. * Returns true if the op is an insert
  18. *
  19. * @param {Op} op
  20. * @returns {op is DeleteOp}
  21. */
  22. function isDelete(op) {
  23. return 'd' in op && op.d != null
  24. }
  25. /**
  26. * Returns true if the op is a comment
  27. *
  28. * @param {Op} op
  29. * @returns {op is CommentOp}
  30. */
  31. function isComment(op) {
  32. return 'c' in op && op.c != null
  33. }
  34. /**
  35. * Get the length of a document from its lines
  36. *
  37. * @param {string[]} lines
  38. * @returns {number}
  39. */
  40. function getDocLength(lines) {
  41. let docLength = _.reduce(lines, (chars, line) => chars + line.length, 0)
  42. // Add newline characters. Lines are joined by newlines, but the last line
  43. // doesn't include a newline. We must make a special case for an empty list
  44. // so that it doesn't report a doc length of -1.
  45. docLength += Math.max(lines.length - 1, 0)
  46. return docLength
  47. }
  48. /**
  49. * Adds given tracked deletes to the given content.
  50. *
  51. * The history system includes tracked deletes in the document content.
  52. *
  53. * @param {string} content
  54. * @param {TrackedChange[]} trackedChanges
  55. * @return {string} content for the history service
  56. */
  57. function addTrackedDeletesToContent(content, trackedChanges) {
  58. let cursor = 0
  59. let result = ''
  60. for (const change of trackedChanges) {
  61. if (isDelete(change.op)) {
  62. // Add the content before the tracked delete
  63. result += content.slice(cursor, change.op.p)
  64. cursor = change.op.p
  65. // Add the content of the tracked delete
  66. result += change.op.d
  67. }
  68. }
  69. // Add the content after all tracked deletes
  70. result += content.slice(cursor)
  71. return result
  72. }
  73. /**
  74. * Compute the content hash for a doc
  75. *
  76. * This hash is sent to the history to validate updates.
  77. *
  78. * @param {string[]} lines
  79. * @return {string} the doc hash
  80. */
  81. function computeDocHash(lines) {
  82. const hash = createHash('sha1')
  83. if (lines.length > 0) {
  84. for (const line of lines.slice(0, lines.length - 1)) {
  85. hash.update(line)
  86. hash.update('\n')
  87. }
  88. // The last line doesn't end with a newline
  89. hash.update(lines[lines.length - 1])
  90. }
  91. return hash.digest('hex')
  92. }
  93. /**
  94. * checks if the given originOrSource should be treated as a source or origin
  95. * TODO: remove this hack and remove all "source" references
  96. */
  97. function extractOriginOrSource(originOrSource) {
  98. let source = null
  99. let origin = null
  100. if (typeof originOrSource === 'string') {
  101. source = originOrSource
  102. } else if (originOrSource && typeof originOrSource === 'object') {
  103. origin = originOrSource
  104. }
  105. return { source, origin }
  106. }
  107. module.exports = {
  108. isInsert,
  109. isDelete,
  110. isComment,
  111. addTrackedDeletesToContent,
  112. getDocLength,
  113. computeDocHash,
  114. extractOriginOrSource,
  115. }