From 8348e0fbf12fa88c776cb1dbf05473845c669d6b Mon Sep 17 00:00:00 2001 From: lawvs <18554747+lawvs@users.noreply.github.com> Date: Mon, 15 Apr 2024 20:26:49 +0800 Subject: [PATCH] feat: export json --- src/components/export-button.tsx | 13 ++++++ src/utils.ts | 23 ++++++++++ src/y-shape.ts | 79 +++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/components/export-button.tsx b/src/components/export-button.tsx index 1920cff..ad947c8 100644 --- a/src/components/export-button.tsx +++ b/src/components/export-button.tsx @@ -8,6 +8,7 @@ import { import { Download } from "lucide-react"; import * as Y from "yjs"; import { useYDoc } from "../state"; +import { yShapeToJSON } from "../y-shape"; function downloadFile(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); @@ -64,6 +65,18 @@ export function ExportButton() { > Snapshot + { + const json = yShapeToJSON(yDoc); + const jsonStr = JSON.stringify(json, null, 2); + const blob = new Blob([jsonStr], { + type: "application/json", + }); + downloadFile(blob, "ydoc-json"); + }} + > + JSON(unofficial) + ); diff --git a/src/utils.ts b/src/utils.ts index 1033999..492e1c2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -37,3 +37,26 @@ export const or = export function getHumanReadablePath(path: Path) { return ["root", ...path].join("."); } + +/** + * This function should never be called. If it is called, it means that the + * code has reached a point that should be unreachable. + * + * @example + * ```ts + * function f(val: 'a' | 'b') { + * if (val === 'a') { + * return 1; + * } else if (val === 'b') { + * return 2; + * } + * unreachable(val); + * ``` + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function unreachable( + _val: never, + message = "Unreachable code reached", +): never { + throw new Error(message); +} diff --git a/src/y-shape.ts b/src/y-shape.ts index dab6d86..f098d17 100644 --- a/src/y-shape.ts +++ b/src/y-shape.ts @@ -1,6 +1,6 @@ import type { Path } from "@textea/json-viewer"; import * as Y from "yjs"; -import { getPathValue, or } from "./utils"; +import { getPathValue, or, unreachable } from "./utils"; /** * Guess AbstractType @@ -114,7 +114,9 @@ export function isYAbstractType( * * See also {@link isYAbstractType} */ -export function isYShape(value: unknown): value is Y.AbstractType { +export function isYShape( + value: unknown, +): value is Y.AbstractType | Y.Doc { return or(isYDoc, isYAbstractType)(value); } @@ -185,6 +187,79 @@ export function parseYShape( return value; } +export const NATIVE_UNIQ_IDENTIFIER = "$yjs:internal:native$"; + +export function yShapeToJSON( + value: any, +): object | string | number | boolean | null | undefined { + if (!isYShape(value)) { + return value; + } + const typeName = getYTypeName(value); + + if (isYDoc(value)) { + const yDoc = value; + const keys = Array.from(yDoc.share.keys()); + const obj = keys.reduce( + (acc, key) => { + const val = yDoc.get(key); + const type = guessType(val); + acc[key] = yShapeToJSON(yDoc.get(key, type)); + return acc; + }, + { + [NATIVE_UNIQ_IDENTIFIER]: typeName, + } as Record, + ); + return obj; + } + if (isYMap(value)) { + const yMap = value; + const keys = Array.from(yMap.keys()); + const obj = keys.reduce( + (acc, key) => { + acc[key] = yShapeToJSON(yMap.get(key)); + return acc; + }, + { + [NATIVE_UNIQ_IDENTIFIER]: typeName, + } as Record, + ); + return obj; + } + if (isYArray(value)) { + return { + [NATIVE_UNIQ_IDENTIFIER]: typeName, + value: value.toArray().map((value) => yShapeToJSON(value)), + }; + } + if (isYText(value)) { + return { + [NATIVE_UNIQ_IDENTIFIER]: typeName, + delta: value.toDelta(), + }; + } + if (isYXmlElement(value)) { + return { + [NATIVE_UNIQ_IDENTIFIER]: typeName, + nodeName: value.nodeName, + attributes: value.getAttributes(), + }; + } + if (isYXmlFragment(value)) { + return { + [NATIVE_UNIQ_IDENTIFIER]: typeName, + value: value.toJSON(), + }; + } + if (isYAbstractType(value)) { + console.error("Unsupported Yjs type: " + typeName, value); + throw new Error("Unsupported Yjs type: " + typeName); + } + console.error("Unknown Yjs type", value); + unreachable(value, "Unknown Yjs type"); +} + export function getYTypeFromPath(yDoc: Y.Doc, path: Path): unknown { return getPathValue(yDoc, path, (obj: unknown, key) => { if (isYDoc(obj)) {