/ cabbages.ts
cabbages.ts
  1  import type {Patch as AutomergePatch} from "@automerge/automerge-repo/slim"
  2  
  3  /**
  4   * A way to describe move, copy, wrap, cherry-pick and rich text
  5   * @see https://braid.org/meeting-62/portals
  6   */
  7  export interface Portal {
  8  	start: number
  9  	end: number
 10  	path: PathPart[]
 11  	referencing: PatchVersion
 12  }
 13  
 14  /*
 15   * potential for confusion: a PatchVersion is also the document version with
 16   * that patch applied?
 17   */
 18  
 19  export type PatchVersion = string & {"patch-version": true}
 20  export interface PatchSet {
 21  	version: PatchVersion
 22  	parents: PatchVersion[]
 23  	mergeType: string
 24  	patches: Patch[]
 25  	meta: Record<string, string | number | (string | number)[]>
 26  }
 27  
 28  export type PathPart = number | string
 29  
 30  type PatchType<T extends string> = T & {"patch-type": T}
 31  function type<T extends string>(string: T) {
 32  	return string as PatchType<T>
 33  }
 34  
 35  export type PatchRange = [number?, number?] | PathPart
 36  
 37  type Patch = [path: PathPart[], range: PatchRange, val?: any]
 38  
 39  /**
 40   * walk a path in an obj, optionally mutating it to insert missing parts
 41   * for the inspiration of @see https://github.com/braid-org/braid-spec/blob/aea85367d60793c113bdb305a4b4ecf55d38061d/draft-toomim-httpbis-range-patch-01.txt
 42   *
 43   * to insert a string in an array, you need to wrap it in []
 44   *	to insert an array in an array you need to wrap it in []
 45   */
 46  
 47  export function apply<T>(
 48  	path: PathPart[],
 49  	target: any,
 50  	range: PatchRange,
 51  	val?: any,
 52  	reviver?: (
 53  		value: any,
 54  		key: any,
 55  		parent: any,
 56  		path: PathPart[],
 57  		obj: T,
 58  		range: PatchRange
 59  	) => void
 60  ) {
 61  	let originalObject = target
 62  	let p = [...path]
 63  
 64  	while (true) {
 65  		let key = p.shift()
 66  		if (!p.length) {
 67  			if (typeof reviver == "function") {
 68  				val = reviver(val, key, target, path, originalObject, range)
 69  			}
 70  			if (Array.isArray(range) || typeof range == "number") {
 71  				if (typeof key == "undefined") {
 72  					throw new Error("cant treat top level as a seq")
 73  				}
 74  
 75  				key = key!
 76  				// splice
 77  				let [start, end] = Array.isArray(range) ? range : [range, range + 1]
 78  				const ZERO_LENGTH = Array.isArray(range) && range.length == 0
 79  
 80  				if (!ZERO_LENGTH && (start == null || end == null)) {
 81  					throw new RangeError("it's all or nothing, no half measures")
 82  				}
 83  				const DELETE = typeof val == "undefined"
 84  				const INSERT = start === end && !DELETE
 85  				const APPEND = ZERO_LENGTH && !DELETE
 86  				let op = DELETE
 87  					? ("del" as const)
 88  					: APPEND
 89  						? ("add" as const)
 90  						: INSERT
 91  							? ("ins" as const)
 92  							: ("replace" as const)
 93  
 94  				if (typeof target[key] == "undefined") {
 95  					// todo what if it's a function that would return a string?
 96  					if (typeof val == "string") {
 97  						target[key] = ""
 98  					} else {
 99  						target[key] = []
100  					}
101  				}
102  				let seq = target[key]
103  
104  				if (Array.isArray(seq)) {
105  					switch (op) {
106  						case "add": {
107  							Array.isArray(val) ? seq.push(...val) : seq.push(val)
108  							return
109  						}
110  						case "replace":
111  						case "ins": {
112  							Array.isArray(val)
113  								? seq.splice(start!, end! - 1, ...val)
114  								: seq.splice(start!, end! - 1, val)
115  							return
116  						}
117  						case "del": {
118  							seq.splice(start!, end! - start!)
119  							return
120  						}
121  						default: {
122  							throw new Error("i don't know what happened")
123  						}
124  					}
125  				}
126  
127  				if (typeof seq == "string") {
128  					switch (op) {
129  						case "add": {
130  							target[key] = seq + val
131  							return
132  						}
133  						case "replace":
134  						case "ins": {
135  							target[key] = seq.slice(0, start) + val + seq.slice(end)
136  							return
137  						}
138  						case "del": {
139  							target[key] = seq.slice(0, start) + seq.slice(end)
140  							return
141  						}
142  						default: {
143  							throw new Error("i don't know what happened")
144  						}
145  					}
146  				}
147  				// todo should impl for typed arrays?
148  				throw new Error("not implemented")
149  			}
150  
151  			if (typeof key == "undefined") {
152  				if (typeof range != "string") {
153  					throw new Error(`can't index top-level map with ${range}`)
154  				}
155  				target[range] = val
156  				return
157  			}
158  			if (typeof target[key] == "undefined") {
159  				target[key] = {}
160  			}
161  			// put/delete
162  			if (typeof val == "undefined") {
163  				delete target[key][range]
164  			} else {
165  				target[key][range] = val
166  			}
167  
168  			return
169  		}
170  
171  		if (typeof key == "undefined") {
172  			throw new Error("cant treat top level as a seq")
173  		}
174  
175  		key = key!
176  		let nextkey = p[0]
177  		if (typeof target[key] == "undefined") {
178  			if (typeof nextkey == "string") {
179  				target[key] = {}
180  			} else if (typeof nextkey == "number") {
181  				target[key] = []
182  			} else {
183  				throw new Error(`can't go down this road ${target}.${key}.${nextkey}`)
184  			}
185  		}
186  
187  		target = target[key]
188  	}
189  }
190  
191  export const patch = apply
192  
193  class OperationError extends Error {}
194  
195  export function fromAutomerge(autopatch: AutomergePatch): Patch
196  export function fromAutomerge(
197  	autopatch: AutomergePatch,
198  	cb: (...args: Patch) => void
199  ): void
200  export function fromAutomerge(
201  	autopatch: AutomergePatch,
202  	cb?: (...args: Patch) => void
203  ) {
204  	let path = autopatch.path.slice(0, -1)
205  	let key = autopatch.path[autopatch.path.length - 1]
206  
207  	switch (autopatch.action) {
208  		case "conflict":
209  		case "inc":
210  		case "mark":
211  		case "unmark":
212  			throw new OperationError(`can't handle this: ${autopatch.action}`)
213  		case "del": {
214  			return cb
215  				? cb(path, [key as number, +key + (autopatch.length || 0)])
216  				: [path, [key as number, +key + (autopatch.length || 0)]]
217  		}
218  		case "insert": {
219  			return cb
220  				? cb(path, [key as number, key as number], autopatch.values)
221  				: [path, [key as number, key as number], autopatch.values]
222  		}
223  		case "splice": {
224  			return cb
225  				? cb(path, [key as number, key as number], [autopatch.value])
226  				: [path, [key as number, key as number], [autopatch.value]]
227  		}
228  		case "put": {
229  			return cb
230  				? cb(path, key!, autopatch.value)
231  				: [path, key!, autopatch.value]
232  		}
233  	}
234  }