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)) {