text-composable.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. * DS205: Consider reworking code to avoid use of IIFEs
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. // An alternate composable implementation for text. This is much closer
  18. // to the implementation used by google wave.
  19. //
  20. // Ops are lists of components which iterate over the whole document.
  21. // Components are either:
  22. // A number N: Skip N characters in the original document
  23. // {i:'str'}: Insert 'str' at the current position in the document
  24. // {d:'str'}: Delete 'str', which appears at the current position in the document
  25. //
  26. // Eg: [3, {i:'hi'}, 5, {d:'internet'}]
  27. //
  28. // Snapshots are strings.
  29. let makeAppend
  30. const p = function () {} // require('util').debug
  31. const i = function () {} // require('util').inspect
  32. const exports = typeof WEB !== 'undefined' && WEB !== null ? {} : module.exports
  33. exports.name = 'text-composable'
  34. exports.create = () => ''
  35. // -------- Utility methods
  36. const checkOp = function (op) {
  37. if (!Array.isArray(op)) {
  38. throw new Error('Op must be an array of components')
  39. }
  40. let last = null
  41. return (() => {
  42. const result = []
  43. for (const c of Array.from(op)) {
  44. if (typeof c === 'object') {
  45. if (
  46. (c.i == null || !(c.i.length > 0)) &&
  47. (c.d == null || !(c.d.length > 0))
  48. ) {
  49. throw new Error(`Invalid op component: ${i(c)}`)
  50. }
  51. } else {
  52. if (typeof c !== 'number') {
  53. throw new Error('Op components must be objects or numbers')
  54. }
  55. if (!(c > 0)) {
  56. throw new Error('Skip components must be a positive number')
  57. }
  58. if (typeof last === 'number') {
  59. throw new Error('Adjacent skip components should be added')
  60. }
  61. }
  62. result.push((last = c))
  63. }
  64. return result
  65. })()
  66. }
  67. // Makes a function for appending components to a given op.
  68. // Exported for the randomOpGenerator.
  69. exports._makeAppend = makeAppend = op =>
  70. function (component) {
  71. if (component === 0 || component.i === '' || component.d === '') {
  72. return
  73. }
  74. if (op.length === 0) {
  75. return op.push(component)
  76. } else if (
  77. typeof component === 'number' &&
  78. typeof op[op.length - 1] === 'number'
  79. ) {
  80. return (op[op.length - 1] += component)
  81. } else if (component.i != null && op[op.length - 1].i != null) {
  82. return (op[op.length - 1].i += component.i)
  83. } else if (component.d != null && op[op.length - 1].d != null) {
  84. return (op[op.length - 1].d += component.d)
  85. } else {
  86. return op.push(component)
  87. }
  88. }
  89. // checkOp op
  90. // Makes 2 functions for taking components from the start of an op, and for peeking
  91. // at the next op that could be taken.
  92. const makeTake = function (op) {
  93. // The index of the next component to take
  94. let idx = 0
  95. // The offset into the component
  96. let offset = 0
  97. // Take up to length n from the front of op. If n is null, take the next
  98. // op component. If indivisableField == 'd', delete components won't be separated.
  99. // If indivisableField == 'i', insert components won't be separated.
  100. const take = function (n, indivisableField) {
  101. let c
  102. if (idx === op.length) {
  103. return null
  104. }
  105. // assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'
  106. if (typeof op[idx] === 'number') {
  107. if (n == null || op[idx] - offset <= n) {
  108. c = op[idx] - offset
  109. ++idx
  110. offset = 0
  111. return c
  112. } else {
  113. offset += n
  114. return n
  115. }
  116. } else {
  117. // Take from the string
  118. const field = op[idx].i ? 'i' : 'd'
  119. c = {}
  120. if (
  121. n == null ||
  122. op[idx][field].length - offset <= n ||
  123. field === indivisableField
  124. ) {
  125. c[field] = op[idx][field].slice(offset)
  126. ++idx
  127. offset = 0
  128. } else {
  129. c[field] = op[idx][field].slice(offset, offset + n)
  130. offset += n
  131. }
  132. return c
  133. }
  134. }
  135. const peekType = () => op[idx]
  136. return [take, peekType]
  137. }
  138. // Find and return the length of an op component
  139. const componentLength = function (component) {
  140. if (typeof component === 'number') {
  141. return component
  142. } else if (component.i != null) {
  143. return component.i.length
  144. } else {
  145. return component.d.length
  146. }
  147. }
  148. // Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
  149. // adjacent inserts and deletes.
  150. exports.normalize = function (op) {
  151. const newOp = []
  152. const append = makeAppend(newOp)
  153. for (const component of Array.from(op)) {
  154. append(component)
  155. }
  156. return newOp
  157. }
  158. // Apply the op to the string. Returns the new string.
  159. exports.apply = function (str, op) {
  160. p(`Applying ${i(op)} to '${str}'`)
  161. if (typeof str !== 'string') {
  162. throw new Error('Snapshot should be a string')
  163. }
  164. checkOp(op)
  165. const pos = 0
  166. const newDoc = []
  167. for (const component of Array.from(op)) {
  168. if (typeof component === 'number') {
  169. if (component > str.length) {
  170. throw new Error('The op is too long for this document')
  171. }
  172. newDoc.push(str.slice(0, component))
  173. str = str.slice(component)
  174. } else if (component.i != null) {
  175. newDoc.push(component.i)
  176. } else {
  177. if (component.d !== str.slice(0, component.d.length)) {
  178. throw new Error(
  179. `The deleted text '${
  180. component.d
  181. }' doesn't match the next characters in the document '${str.slice(
  182. 0,
  183. component.d.length
  184. )}'`
  185. )
  186. }
  187. str = str.slice(component.d.length)
  188. }
  189. }
  190. if (str !== '') {
  191. throw new Error("The applied op doesn't traverse the entire document")
  192. }
  193. return newDoc.join('')
  194. }
  195. // transform op1 by op2. Return transformed version of op1.
  196. // op1 and op2 are unchanged by transform.
  197. exports.transform = function (op, otherOp, side) {
  198. let component
  199. if (side !== 'left' && side !== 'right') {
  200. throw new Error(`side (${side} must be 'left' or 'right'`)
  201. }
  202. checkOp(op)
  203. checkOp(otherOp)
  204. const newOp = []
  205. const append = makeAppend(newOp)
  206. const [take, peek] = Array.from(makeTake(op))
  207. for (component of Array.from(otherOp)) {
  208. let chunk, length
  209. if (typeof component === 'number') {
  210. // Skip
  211. length = component
  212. while (length > 0) {
  213. chunk = take(length, 'i')
  214. if (chunk === null) {
  215. throw new Error(
  216. 'The op traverses more elements than the document has'
  217. )
  218. }
  219. append(chunk)
  220. if (typeof chunk !== 'object' || chunk.i == null) {
  221. length -= componentLength(chunk)
  222. }
  223. }
  224. } else if (component.i != null) {
  225. // Insert
  226. if (side === 'left') {
  227. // The left insert should go first.
  228. const o = peek()
  229. if (o != null ? o.i : undefined) {
  230. append(take())
  231. }
  232. }
  233. // Otherwise, skip the inserted text.
  234. append(component.i.length)
  235. } else {
  236. // Delete.
  237. // assert.ok component.d
  238. ;({ length } = component.d)
  239. while (length > 0) {
  240. chunk = take(length, 'i')
  241. if (chunk === null) {
  242. throw new Error(
  243. 'The op traverses more elements than the document has'
  244. )
  245. }
  246. if (typeof chunk === 'number') {
  247. length -= chunk
  248. } else if (chunk.i != null) {
  249. append(chunk)
  250. } else {
  251. // assert.ok chunk.d
  252. // The delete is unnecessary now.
  253. length -= chunk.d.length
  254. }
  255. }
  256. }
  257. }
  258. // Append extras from op1
  259. while ((component = take())) {
  260. if ((component != null ? component.i : undefined) == null) {
  261. throw new Error(`Remaining fragments in the op: ${i(component)}`)
  262. }
  263. append(component)
  264. }
  265. return newOp
  266. }
  267. // Compose 2 ops into 1 op.
  268. exports.compose = function (op1, op2) {
  269. let component
  270. p(`COMPOSE ${i(op1)} + ${i(op2)}`)
  271. checkOp(op1)
  272. checkOp(op2)
  273. const result = []
  274. const append = makeAppend(result)
  275. const [take, _] = Array.from(makeTake(op1))
  276. for (component of Array.from(op2)) {
  277. let chunk, length
  278. if (typeof component === 'number') {
  279. // Skip
  280. length = component
  281. while (length > 0) {
  282. chunk = take(length, 'd')
  283. if (chunk === null) {
  284. throw new Error(
  285. 'The op traverses more elements than the document has'
  286. )
  287. }
  288. append(chunk)
  289. if (typeof chunk !== 'object' || chunk.d == null) {
  290. length -= componentLength(chunk)
  291. }
  292. }
  293. } else if (component.i != null) {
  294. // Insert
  295. append({ i: component.i })
  296. } else {
  297. // Delete
  298. let offset = 0
  299. while (offset < component.d.length) {
  300. chunk = take(component.d.length - offset, 'd')
  301. if (chunk === null) {
  302. throw new Error(
  303. 'The op traverses more elements than the document has'
  304. )
  305. }
  306. // If its delete, append it. If its skip, drop it and decrease length. If its insert, check the strings match, drop it and decrease length.
  307. if (typeof chunk === 'number') {
  308. append({ d: component.d.slice(offset, offset + chunk) })
  309. offset += chunk
  310. } else if (chunk.i != null) {
  311. if (component.d.slice(offset, offset + chunk.i.length) !== chunk.i) {
  312. throw new Error("The deleted text doesn't match the inserted text")
  313. }
  314. offset += chunk.i.length
  315. // The ops cancel each other out.
  316. } else {
  317. // Delete
  318. append(chunk)
  319. }
  320. }
  321. }
  322. }
  323. // Append extras from op1
  324. while ((component = take())) {
  325. if ((component != null ? component.d : undefined) == null) {
  326. throw new Error(`Trailing stuff in op1 ${i(component)}`)
  327. }
  328. append(component)
  329. }
  330. return result
  331. }
  332. const invertComponent = function (c) {
  333. if (typeof c === 'number') {
  334. return c
  335. } else if (c.i != null) {
  336. return { d: c.i }
  337. } else {
  338. return { i: c.d }
  339. }
  340. }
  341. // Invert an op
  342. exports.invert = function (op) {
  343. const result = []
  344. const append = makeAppend(result)
  345. for (const component of Array.from(op)) {
  346. append(invertComponent(component))
  347. }
  348. return result
  349. }
  350. if (typeof window !== 'undefined' && window !== null) {
  351. if (!window.ot) {
  352. window.ot = {}
  353. }
  354. if (!window.ot.types) {
  355. window.ot.types = {}
  356. }
  357. window.ot.types.text = exports
  358. }