Skip to content

Commit

Permalink
feat: export json
Browse files Browse the repository at this point in the history
  • Loading branch information
lawvs committed Apr 15, 2024
1 parent cde6c2c commit 8348e0f
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 2 deletions.
13 changes: 13 additions & 0 deletions src/components/export-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -64,6 +65,18 @@ export function ExportButton() {
>
Snapshot
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
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)
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
Expand Down
23 changes: 23 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
79 changes: 77 additions & 2 deletions src/y-shape.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -114,7 +114,9 @@ export function isYAbstractType(
*
* See also {@link isYAbstractType}
*/
export function isYShape(value: unknown): value is Y.AbstractType<unknown> {
export function isYShape(
value: unknown,
): value is Y.AbstractType<unknown> | Y.Doc {
return or(isYDoc, isYAbstractType)(value);
}

Expand Down Expand Up @@ -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<string, unknown>,
);
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<string, unknown>,
);
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)) {
Expand Down

0 comments on commit 8348e0f

Please sign in to comment.