text.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /* eslint-disable
  2. camelcase,
  3. no-return-assign,
  4. no-undef,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. // A simple text implementation
  16. //
  17. // Operations are lists of components.
  18. // Each component either inserts or deletes at a specified position in the document.
  19. //
  20. // Components are either:
  21. // {i:'str', p:100}: Insert 'str' at position 100 in the document
  22. // {d:'str', p:100}: Delete 'str' at position 100 in the document
  23. //
  24. // Components in an operation are executed sequentially, so the position of components
  25. // assumes previous components have already executed.
  26. //
  27. // Eg: This op:
  28. // [{i:'abc', p:0}]
  29. // is equivalent to this op:
  30. // [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]
  31. // NOTE: The global scope here is shared with other sharejs files when built with closure.
  32. // Be careful what ends up in your namespace.
  33. let append, transformComponent
  34. const text = {}
  35. text.name = 'text'
  36. text.create = () => ''
  37. const strInject = (s1, pos, s2) => s1.slice(0, pos) + s2 + s1.slice(pos)
  38. const checkValidComponent = function (c) {
  39. if (typeof c.p !== 'number') {
  40. throw new Error('component missing position field')
  41. }
  42. const i_type = typeof c.i
  43. const d_type = typeof c.d
  44. if (!((i_type === 'string') ^ (d_type === 'string'))) {
  45. throw new Error('component needs an i or d field')
  46. }
  47. if (!(c.p >= 0)) {
  48. throw new Error('position cannot be negative')
  49. }
  50. }
  51. const checkValidOp = function (op) {
  52. for (const c of Array.from(op)) {
  53. checkValidComponent(c)
  54. }
  55. return true
  56. }
  57. text.apply = function (snapshot, op) {
  58. checkValidOp(op)
  59. for (const component of Array.from(op)) {
  60. if (component.i != null) {
  61. snapshot = strInject(snapshot, component.p, component.i)
  62. } else {
  63. const deleted = snapshot.slice(
  64. component.p,
  65. component.p + component.d.length
  66. )
  67. if (component.d !== deleted) {
  68. throw new Error(
  69. `Delete component '${component.d}' does not match deleted text '${deleted}'`
  70. )
  71. }
  72. snapshot =
  73. snapshot.slice(0, component.p) +
  74. snapshot.slice(component.p + component.d.length)
  75. }
  76. }
  77. return snapshot
  78. }
  79. // Exported for use by the random op generator.
  80. //
  81. // For simplicity, this version of append does not compress adjacent inserts and deletes of
  82. // the same text. It would be nice to change that at some stage.
  83. text._append = append = function (newOp, c) {
  84. if (c.i === '' || c.d === '') {
  85. return
  86. }
  87. if (newOp.length === 0) {
  88. return newOp.push(c)
  89. } else {
  90. const last = newOp[newOp.length - 1]
  91. // Compose the insert into the previous insert if possible
  92. if (
  93. last.i != null &&
  94. c.i != null &&
  95. last.p <= c.p &&
  96. c.p <= last.p + last.i.length
  97. ) {
  98. return (newOp[newOp.length - 1] = {
  99. i: strInject(last.i, c.p - last.p, c.i),
  100. p: last.p,
  101. })
  102. } else if (
  103. last.d != null &&
  104. c.d != null &&
  105. c.p <= last.p &&
  106. last.p <= c.p + c.d.length
  107. ) {
  108. return (newOp[newOp.length - 1] = {
  109. d: strInject(c.d, last.p - c.p, last.d),
  110. p: c.p,
  111. })
  112. } else {
  113. return newOp.push(c)
  114. }
  115. }
  116. }
  117. text.compose = function (op1, op2) {
  118. checkValidOp(op1)
  119. checkValidOp(op2)
  120. const newOp = op1.slice()
  121. for (const c of Array.from(op2)) {
  122. append(newOp, c)
  123. }
  124. return newOp
  125. }
  126. // Attempt to compress the op components together 'as much as possible'.
  127. // This implementation preserves order and preserves create/delete pairs.
  128. text.compress = op => text.compose([], op)
  129. text.normalize = function (op) {
  130. const newOp = []
  131. // Normalize should allow ops which are a single (unwrapped) component:
  132. // {i:'asdf', p:23}.
  133. // There's no good way to test if something is an array:
  134. // http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
  135. // so this is probably the least bad solution.
  136. if (op.i != null || op.p != null) {
  137. op = [op]
  138. }
  139. for (const c of Array.from(op)) {
  140. if (c.p == null) {
  141. c.p = 0
  142. }
  143. append(newOp, c)
  144. }
  145. return newOp
  146. }
  147. // This helper method transforms a position by an op component.
  148. //
  149. // If c is an insert, insertAfter specifies whether the transform
  150. // is pushed after the insert (true) or before it (false).
  151. //
  152. // insertAfter is optional for deletes.
  153. const transformPosition = function (pos, c, insertAfter) {
  154. if (c.i != null) {
  155. if (c.p < pos || (c.p === pos && insertAfter)) {
  156. return pos + c.i.length
  157. } else {
  158. return pos
  159. }
  160. } else {
  161. // I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
  162. // but I think its harder to read that way, and it compiles using ternary operators anyway
  163. // so its no slower written like this.
  164. if (pos <= c.p) {
  165. return pos
  166. } else if (pos <= c.p + c.d.length) {
  167. return c.p
  168. } else {
  169. return pos - c.d.length
  170. }
  171. }
  172. }
  173. // Helper method to transform a cursor position as a result of an op.
  174. //
  175. // Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position
  176. // is pushed after an insert (true) or before it (false).
  177. text.transformCursor = function (position, op, side) {
  178. const insertAfter = side === 'right'
  179. for (const c of Array.from(op)) {
  180. position = transformPosition(position, c, insertAfter)
  181. }
  182. return position
  183. }
  184. // Transform an op component by another op component. Asymmetric.
  185. // The result will be appended to destination.
  186. //
  187. // exported for use in JSON type
  188. text._tc = transformComponent = function (dest, c, otherC, side) {
  189. checkValidOp([c])
  190. checkValidOp([otherC])
  191. if (c.i != null) {
  192. append(dest, {
  193. i: c.i,
  194. p: transformPosition(c.p, otherC, side === 'right'),
  195. })
  196. } else {
  197. // Delete
  198. if (otherC.i != null) {
  199. // delete vs insert
  200. let s = c.d
  201. if (c.p < otherC.p) {
  202. append(dest, { d: s.slice(0, otherC.p - c.p), p: c.p })
  203. s = s.slice(otherC.p - c.p)
  204. }
  205. if (s !== '') {
  206. append(dest, { d: s, p: c.p + otherC.i.length })
  207. }
  208. } else {
  209. // Delete vs delete
  210. if (c.p >= otherC.p + otherC.d.length) {
  211. append(dest, { d: c.d, p: c.p - otherC.d.length })
  212. } else if (c.p + c.d.length <= otherC.p) {
  213. append(dest, c)
  214. } else {
  215. // They overlap somewhere.
  216. const newC = { d: '', p: c.p }
  217. if (c.p < otherC.p) {
  218. newC.d = c.d.slice(0, otherC.p - c.p)
  219. }
  220. if (c.p + c.d.length > otherC.p + otherC.d.length) {
  221. newC.d += c.d.slice(otherC.p + otherC.d.length - c.p)
  222. }
  223. // This is entirely optional - just for a check that the deleted
  224. // text in the two ops matches
  225. const intersectStart = Math.max(c.p, otherC.p)
  226. const intersectEnd = Math.min(
  227. c.p + c.d.length,
  228. otherC.p + otherC.d.length
  229. )
  230. const cIntersect = c.d.slice(intersectStart - c.p, intersectEnd - c.p)
  231. const otherIntersect = otherC.d.slice(
  232. intersectStart - otherC.p,
  233. intersectEnd - otherC.p
  234. )
  235. if (cIntersect !== otherIntersect) {
  236. throw new Error(
  237. 'Delete ops delete different text in the same region of the document'
  238. )
  239. }
  240. if (newC.d !== '') {
  241. // This could be rewritten similarly to insert v delete, above.
  242. newC.p = transformPosition(newC.p, otherC)
  243. append(dest, newC)
  244. }
  245. }
  246. }
  247. }
  248. return dest
  249. }
  250. const invertComponent = function (c) {
  251. if (c.i != null) {
  252. return { d: c.i, p: c.p }
  253. } else {
  254. return { i: c.d, p: c.p }
  255. }
  256. }
  257. // No need to use append for invert, because the components won't be able to
  258. // cancel with one another.
  259. text.invert = op =>
  260. Array.from(op.slice().reverse()).map(c => invertComponent(c))
  261. if (typeof WEB !== 'undefined' && WEB !== null) {
  262. if (!exports.types) {
  263. exports.types = {}
  264. }
  265. // This is kind of awful - come up with a better way to hook this helper code up.
  266. bootstrapTransform(text, transformComponent, checkValidOp, append)
  267. // [] is used to prevent closure from renaming types.text
  268. exports.types.text = text
  269. } else {
  270. module.exports = text
  271. // The text type really shouldn't need this - it should be possible to define
  272. // an efficient transform function by making a sort of transform map and passing each
  273. // op component through it.
  274. require('./helpers').bootstrapTransform(
  275. text,
  276. transformComponent,
  277. checkValidOp,
  278. append
  279. )
  280. }