changeset.ts
1import { err, ok, type Result } from "neverthrow";
2import * as Y from "yjs";
3import { isXmlElement } from "./filter";
4
5type AnyElement = Y.XmlText | Y.XmlElement;
6
7export class Changeset {
8 #changes: Array<() => Result<void, string>>;
9
10 constructor() {
11 this.#changes = [];
12 }
13
14 add(f: () => Result<void, string>) {
15 this.#changes.push(f);
16 }
17 prependText(text: string, to: Y.XmlText) {
18 this.#changes.push(() => {
19 to.insert(0, text);
20 return ok();
21 });
22 }
23 appendText(text: string, to: Y.XmlText) {
24 this.#changes.push(() => {
25 to.insert(to.length, text);
26 return ok();
27 });
28 }
29 insertBefore(putBefore: AnyElement, insert: AnyElement) {
30 if (isArray(putBefore.parent)) {
31 this.insertArray(putBefore, insert, "before");
32 return;
33 }
34 this.#changes.push(() => {
35 if (
36 putBefore.prevSibling === null &&
37 isXmlElement(putBefore.parent)
38 ) {
39 putBefore.parent.unshift([insert]);
40 return ok();
41 } else if (
42 putBefore.prevSibling &&
43 isXmlElement(putBefore.parent)
44 ) {
45 putBefore.parent.insertAfter(putBefore.prevSibling, [insert]);
46 return ok();
47 }
48 return err(
49 `insertBefore failed: parent likely not an XmlElement or Array, parent=${putBefore.parent?.toString()}`,
50 );
51 });
52 }
53 insertAfter(putAfter: AnyElement, insert: AnyElement) {
54 if (isArray(putAfter.parent)) {
55 this.insertArray(putAfter, insert, "after");
56 return;
57 }
58 this.#changes.push(() => {
59 if (isXmlElement(putAfter.parent)) {
60 putAfter.parent.insertAfter(putAfter, [insert]);
61 return ok();
62 }
63 return err(
64 `insertAfter failed: parent likely not an XmlElement or Array, parent = ${putAfter.parent?.toString()}`,
65 );
66 });
67 }
68 private insertArray(
69 putAfter: AnyElement,
70 insert: AnyElement,
71 type: "before" | "after",
72 ) {
73 this.#changes.push(() => {
74 let index: number = 0;
75 const parent = putAfter.parent;
76 if (isArray(parent)) {
77 parent.forEach((n: unknown, i: number) => {
78 if (putAfter === (n as AnyElement)) {
79 index = i;
80 }
81 });
82 const where = type === "before" ? index : index + 1;
83 parent.insert(where, [insert]);
84 return ok();
85 }
86 return err(
87 `attempted insert after in Y.Array, unknown type parent = ${parent?.toString()}`,
88 );
89 });
90 }
91 updateAttributes(on: Y.XmlElement, attrs: Record<string, string>) {
92 this.#changes.push(() => {
93 for (const [key, value] of Object.entries(attrs)) {
94 on.setAttribute(key, value);
95 }
96 return ok();
97 });
98 }
99
100 apply(doc: Y.Doc): Result<void, string> {
101 const errs: Array<string> = [];
102 doc.transact(() => {
103 this.#changes.forEach((f) => {
104 f().mapErr((e) => errs.push(e));
105 });
106 });
107 if (errs.length > 0) {
108 return err(errs.join("; "));
109 }
110 return ok();
111 }
112}
113
114const isArray = (n: unknown): n is Y.Array<unknown> => {
115 return n instanceof Y.Array;
116};