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};