text-tp2.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /* eslint-disable
  2. no-cond-assign,
  3. no-return-assign,
  4. no-undef,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS103: Rewrite code to no longer use __guard__
  14. * DS205: Consider reworking code to avoid use of IIFEs
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. // A TP2 implementation of text, following this spec:
  19. // http://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README
  20. //
  21. // A document is made up of a string and a set of tombstones inserted throughout
  22. // the string. For example, 'some ', (2 tombstones), 'string'.
  23. //
  24. // This is encoded in a document as: {s:'some string', t:[5, -2, 6]}
  25. //
  26. // Ops are lists of components which iterate over the whole document.
  27. // Components are either:
  28. // N: Skip N characters in the original document
  29. // {i:'str'}: Insert 'str' at the current position in the document
  30. // {i:N}: Insert N tombstones at the current position in the document
  31. // {d:N}: Delete (tombstone) N characters at the current position in the document
  32. //
  33. // Eg: [3, {i:'hi'}, 5, {d:8}]
  34. //
  35. // Snapshots are lists with characters and tombstones. Characters are stored in strings
  36. // and adjacent tombstones are flattened into numbers.
  37. //
  38. // Eg, the document: 'Hello .....world' ('.' denotes tombstoned (deleted) characters)
  39. // would be represented by a document snapshot of ['Hello ', 5, 'world']
  40. let append, appendDoc, takeDoc
  41. const type = {
  42. name: 'text-tp2',
  43. tp2: true,
  44. create() {
  45. return { charLength: 0, totalLength: 0, positionCache: [], data: [] }
  46. },
  47. serialize(doc) {
  48. if (!doc.data) {
  49. throw new Error('invalid doc snapshot')
  50. }
  51. return doc.data
  52. },
  53. deserialize(data) {
  54. const doc = type.create()
  55. doc.data = data
  56. for (const component of Array.from(data)) {
  57. if (typeof component === 'string') {
  58. doc.charLength += component.length
  59. doc.totalLength += component.length
  60. } else {
  61. doc.totalLength += component
  62. }
  63. }
  64. return doc
  65. },
  66. }
  67. const checkOp = function (op) {
  68. if (!Array.isArray(op)) {
  69. throw new Error('Op must be an array of components')
  70. }
  71. let last = null
  72. return (() => {
  73. const result = []
  74. for (const c of Array.from(op)) {
  75. if (typeof c === 'object') {
  76. if (c.i !== undefined) {
  77. if (
  78. (typeof c.i !== 'string' || !(c.i.length > 0)) &&
  79. (typeof c.i !== 'number' || !(c.i > 0))
  80. ) {
  81. throw new Error('Inserts must insert a string or a +ive number')
  82. }
  83. } else if (c.d !== undefined) {
  84. if (typeof c.d !== 'number' || !(c.d > 0)) {
  85. throw new Error('Deletes must be a +ive number')
  86. }
  87. } else {
  88. throw new Error('Operation component must define .i or .d')
  89. }
  90. } else {
  91. if (typeof c !== 'number') {
  92. throw new Error('Op components must be objects or numbers')
  93. }
  94. if (!(c > 0)) {
  95. throw new Error('Skip components must be a positive number')
  96. }
  97. if (typeof last === 'number') {
  98. throw new Error('Adjacent skip components should be combined')
  99. }
  100. }
  101. result.push((last = c))
  102. }
  103. return result
  104. })()
  105. }
  106. // Take the next part from the specified position in a document snapshot.
  107. // position = {index, offset}. It will be updated.
  108. type._takeDoc = takeDoc = function (
  109. doc,
  110. position,
  111. maxlength,
  112. tombsIndivisible
  113. ) {
  114. if (position.index >= doc.data.length) {
  115. throw new Error('Operation goes past the end of the document')
  116. }
  117. const part = doc.data[position.index]
  118. // peel off data[0]
  119. const result =
  120. typeof part === 'string'
  121. ? maxlength !== undefined
  122. ? part.slice(position.offset, position.offset + maxlength)
  123. : part.slice(position.offset)
  124. : maxlength === undefined || tombsIndivisible
  125. ? part - position.offset
  126. : Math.min(maxlength, part - position.offset)
  127. const resultLen = result.length || result
  128. if ((part.length || part) - position.offset > resultLen) {
  129. position.offset += resultLen
  130. } else {
  131. position.index++
  132. position.offset = 0
  133. }
  134. return result
  135. }
  136. // Append a part to the end of a document
  137. type._appendDoc = appendDoc = function (doc, p) {
  138. if (p === 0 || p === '') {
  139. return
  140. }
  141. if (typeof p === 'string') {
  142. doc.charLength += p.length
  143. doc.totalLength += p.length
  144. } else {
  145. doc.totalLength += p
  146. }
  147. const { data } = doc
  148. if (data.length === 0) {
  149. data.push(p)
  150. } else if (typeof data[data.length - 1] === typeof p) {
  151. data[data.length - 1] += p
  152. } else {
  153. data.push(p)
  154. }
  155. }
  156. // Apply the op to the document. The document is not modified in the process.
  157. type.apply = function (doc, op) {
  158. if (
  159. doc.totalLength === undefined ||
  160. doc.charLength === undefined ||
  161. doc.data.length === undefined
  162. ) {
  163. throw new Error('Snapshot is invalid')
  164. }
  165. checkOp(op)
  166. const newDoc = type.create()
  167. const position = { index: 0, offset: 0 }
  168. for (const component of Array.from(op)) {
  169. let part, remainder
  170. if (typeof component === 'number') {
  171. remainder = component
  172. while (remainder > 0) {
  173. part = takeDoc(doc, position, remainder)
  174. appendDoc(newDoc, part)
  175. remainder -= part.length || part
  176. }
  177. } else if (component.i !== undefined) {
  178. appendDoc(newDoc, component.i)
  179. } else if (component.d !== undefined) {
  180. remainder = component.d
  181. while (remainder > 0) {
  182. part = takeDoc(doc, position, remainder)
  183. remainder -= part.length || part
  184. }
  185. appendDoc(newDoc, component.d)
  186. }
  187. }
  188. return newDoc
  189. }
  190. // Append an op component to the end of the specified op.
  191. // Exported for the randomOpGenerator.
  192. type._append = append = function (op, component) {
  193. if (
  194. component === 0 ||
  195. component.i === '' ||
  196. component.i === 0 ||
  197. component.d === 0
  198. ) {
  199. return
  200. }
  201. if (op.length === 0) {
  202. return op.push(component)
  203. } else {
  204. const last = op[op.length - 1]
  205. if (typeof component === 'number' && typeof last === 'number') {
  206. return (op[op.length - 1] += component)
  207. } else if (
  208. component.i !== undefined &&
  209. last.i != null &&
  210. typeof last.i === typeof component.i
  211. ) {
  212. return (last.i += component.i)
  213. } else if (component.d !== undefined && last.d != null) {
  214. return (last.d += component.d)
  215. } else {
  216. return op.push(component)
  217. }
  218. }
  219. }
  220. // Makes 2 functions for taking components from the start of an op, and for peeking
  221. // at the next op that could be taken.
  222. const makeTake = function (op) {
  223. // The index of the next component to take
  224. let index = 0
  225. // The offset into the component
  226. let offset = 0
  227. // Take up to length maxlength from the op. If maxlength is not defined, there is no max.
  228. // If insertsIndivisible is true, inserts (& insert tombstones) won't be separated.
  229. //
  230. // Returns null when op is fully consumed.
  231. const take = function (maxlength, insertsIndivisible) {
  232. let current
  233. if (index === op.length) {
  234. return null
  235. }
  236. const e = op[index]
  237. if (
  238. typeof (current = e) === 'number' ||
  239. typeof (current = e.i) === 'number' ||
  240. (current = e.d) !== undefined
  241. ) {
  242. let c
  243. if (
  244. maxlength == null ||
  245. current - offset <= maxlength ||
  246. (insertsIndivisible && e.i !== undefined)
  247. ) {
  248. // Return the rest of the current element.
  249. c = current - offset
  250. ++index
  251. offset = 0
  252. } else {
  253. offset += maxlength
  254. c = maxlength
  255. }
  256. if (e.i !== undefined) {
  257. return { i: c }
  258. } else if (e.d !== undefined) {
  259. return { d: c }
  260. } else {
  261. return c
  262. }
  263. } else {
  264. // Take from the inserted string
  265. let result
  266. if (
  267. maxlength == null ||
  268. e.i.length - offset <= maxlength ||
  269. insertsIndivisible
  270. ) {
  271. result = { i: e.i.slice(offset) }
  272. ++index
  273. offset = 0
  274. } else {
  275. result = { i: e.i.slice(offset, offset + maxlength) }
  276. offset += maxlength
  277. }
  278. return result
  279. }
  280. }
  281. const peekType = () => op[index]
  282. return [take, peekType]
  283. }
  284. // Find and return the length of an op component
  285. const componentLength = function (component) {
  286. if (typeof component === 'number') {
  287. return component
  288. } else if (typeof component.i === 'string') {
  289. return component.i.length
  290. } else {
  291. // This should work because c.d and c.i must be +ive.
  292. return component.d || component.i
  293. }
  294. }
  295. // Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
  296. // adjacent inserts and deletes.
  297. type.normalize = function (op) {
  298. const newOp = []
  299. for (const component of Array.from(op)) {
  300. append(newOp, component)
  301. }
  302. return newOp
  303. }
  304. // This is a helper method to transform and prune. goForwards is true for transform, false for prune.
  305. const transformer = function (op, otherOp, goForwards, side) {
  306. let component
  307. checkOp(op)
  308. checkOp(otherOp)
  309. const newOp = []
  310. const [take, peek] = Array.from(makeTake(op))
  311. for (component of Array.from(otherOp)) {
  312. let chunk
  313. let length = componentLength(component)
  314. if (component.i !== undefined) {
  315. // Insert text or tombs
  316. if (goForwards) {
  317. // transform - insert skips over inserted parts
  318. if (side === 'left') {
  319. // The left insert should go first.
  320. while (__guard__(peek(), x => x.i) !== undefined) {
  321. append(newOp, take())
  322. }
  323. }
  324. // In any case, skip the inserted text.
  325. append(newOp, length)
  326. } else {
  327. // Prune. Remove skips for inserts.
  328. while (length > 0) {
  329. chunk = take(length, true)
  330. if (chunk === null) {
  331. throw new Error('The transformed op is invalid')
  332. }
  333. if (chunk.d !== undefined) {
  334. throw new Error(
  335. 'The transformed op deletes locally inserted characters - it cannot be purged of the insert.'
  336. )
  337. }
  338. if (typeof chunk === 'number') {
  339. length -= chunk
  340. } else {
  341. append(newOp, chunk)
  342. }
  343. }
  344. }
  345. } else {
  346. // Skip or delete
  347. while (length > 0) {
  348. chunk = take(length, true)
  349. if (chunk === null) {
  350. throw new Error(
  351. 'The op traverses more elements than the document has'
  352. )
  353. }
  354. append(newOp, chunk)
  355. if (!chunk.i) {
  356. length -= componentLength(chunk)
  357. }
  358. }
  359. }
  360. }
  361. // Append extras from op1
  362. while ((component = take())) {
  363. if (component.i === undefined) {
  364. throw new Error(`Remaining fragments in the op: ${component}`)
  365. }
  366. append(newOp, component)
  367. }
  368. return newOp
  369. }
  370. // transform op1 by op2. Return transformed version of op1.
  371. // op1 and op2 are unchanged by transform.
  372. // side should be 'left' or 'right', depending on if op1.id <> op2.id. 'left' == client op.
  373. type.transform = function (op, otherOp, side) {
  374. if (side !== 'left' && side !== 'right') {
  375. throw new Error(`side (${side}) should be 'left' or 'right'`)
  376. }
  377. return transformer(op, otherOp, true, side)
  378. }
  379. // Prune is the inverse of transform.
  380. type.prune = (op, otherOp) => transformer(op, otherOp, false)
  381. // Compose 2 ops into 1 op.
  382. type.compose = function (op1, op2) {
  383. let component
  384. if (op1 === null || op1 === undefined) {
  385. return op2
  386. }
  387. checkOp(op1)
  388. checkOp(op2)
  389. const result = []
  390. const [take, _] = Array.from(makeTake(op1))
  391. for (component of Array.from(op2)) {
  392. let chunk, length
  393. if (typeof component === 'number') {
  394. // Skip
  395. // Just copy from op1.
  396. length = component
  397. while (length > 0) {
  398. chunk = take(length)
  399. if (chunk === null) {
  400. throw new Error(
  401. 'The op traverses more elements than the document has'
  402. )
  403. }
  404. append(result, chunk)
  405. length -= componentLength(chunk)
  406. }
  407. } else if (component.i !== undefined) {
  408. // Insert
  409. append(result, { i: component.i })
  410. } else {
  411. // Delete
  412. length = component.d
  413. while (length > 0) {
  414. chunk = take(length)
  415. if (chunk === null) {
  416. throw new Error(
  417. 'The op traverses more elements than the document has'
  418. )
  419. }
  420. const chunkLength = componentLength(chunk)
  421. if (chunk.i !== undefined) {
  422. append(result, { i: chunkLength })
  423. } else {
  424. append(result, { d: chunkLength })
  425. }
  426. length -= chunkLength
  427. }
  428. }
  429. }
  430. // Append extras from op1
  431. while ((component = take())) {
  432. if (component.i === undefined) {
  433. throw new Error(`Remaining fragments in op1: ${component}`)
  434. }
  435. append(result, component)
  436. }
  437. return result
  438. }
  439. if (typeof WEB !== 'undefined' && WEB !== null) {
  440. exports.types['text-tp2'] = type
  441. } else {
  442. module.exports = type
  443. }
  444. function __guard__(value, transform) {
  445. return typeof value !== 'undefined' && value !== null
  446. ? transform(value)
  447. : undefined
  448. }