tldraw
Version:
A tiny little drawing editor.
221 lines (220 loc) • 6.62 kB
JavaScript
import {
FileHelpers,
MigrationFailureReason,
Result,
T,
createTLStore,
exhaustiveSwitchError,
fetch,
transact
} from "@tldraw/editor";
import { buildFromV1Document } from "../tldr/buildFromV1Document.mjs";
const TLDRAW_FILE_MIMETYPE = "application/vnd.tldraw+json";
const TLDRAW_FILE_EXTENSION = ".tldr";
const LATEST_TLDRAW_FILE_FORMAT_VERSION = 1;
const schemaV1 = T.object({
schemaVersion: T.literal(1),
storeVersion: T.positiveInteger,
recordVersions: T.dict(
T.string,
T.object({
version: T.positiveInteger,
subTypeVersions: T.dict(T.string, T.positiveInteger).optional(),
subTypeKey: T.string.optional()
})
)
});
const schemaV2 = T.object({
schemaVersion: T.literal(2),
sequences: T.dict(T.string, T.positiveInteger)
});
const tldrawFileValidator = T.object({
tldrawFileFormatVersion: T.nonZeroInteger,
schema: T.numberUnion("schemaVersion", {
1: schemaV1,
2: schemaV2
}),
records: T.arrayOf(
T.object({
id: T.string,
typeName: T.string
}).allowUnknownProperties()
)
});
function isV1File(data) {
try {
if (data.document?.version) {
return true;
}
return false;
} catch {
return false;
}
}
function parseTldrawJsonFile({
json,
schema
}) {
let data;
try {
data = tldrawFileValidator.validate(JSON.parse(json));
} catch (e) {
try {
data = JSON.parse(json);
if (isV1File(data)) {
return Result.err({ type: "v1File", data });
}
} catch {
}
return Result.err({ type: "notATldrawFile", cause: e });
}
if (data.tldrawFileFormatVersion > LATEST_TLDRAW_FILE_FORMAT_VERSION) {
return Result.err({
type: "fileFormatVersionTooNew",
version: data.tldrawFileFormatVersion
});
}
let migrationResult;
let storeSnapshot;
try {
const records = pruneUnusedAssets(data.records);
storeSnapshot = Object.fromEntries(records.map((r) => [r.id, r]));
migrationResult = schema.migrateStoreSnapshot({ store: storeSnapshot, schema: data.schema });
} catch (e) {
return Result.err({ type: "invalidRecords", cause: e });
}
if (migrationResult.type === "error") {
return Result.err({ type: "migrationFailed", reason: migrationResult.reason });
}
try {
return Result.ok(
createTLStore({
snapshot: { store: storeSnapshot, schema: data.schema },
schema
})
);
} catch (e) {
return Result.err({ type: "invalidRecords", cause: e });
}
}
function pruneUnusedAssets(records) {
const usedAssets = /* @__PURE__ */ new Set();
for (const record of records) {
if (record.typeName === "shape" && "assetId" in record.props && record.props.assetId) {
usedAssets.add(record.props.assetId);
}
}
return records.filter((r) => r.typeName !== "asset" || usedAssets.has(r.id));
}
async function serializeTldrawJson(editor) {
const records = [];
for (const record of editor.store.allRecords()) {
switch (record.typeName) {
case "asset":
if (record.type !== "bookmark" && record.props.src && !record.props.src.startsWith("data:")) {
let assetSrcToSave;
try {
let src = record.props.src;
if (!src.startsWith("http")) {
src = (await editor.resolveAssetUrl(record.id, { shouldResolveToOriginal: true })) || "";
}
assetSrcToSave = await FileHelpers.blobToDataUrl(await (await fetch(src)).blob());
} catch {
assetSrcToSave = record.props.src;
}
records.push({
...record,
props: {
...record.props,
src: assetSrcToSave
}
});
} else {
records.push(record);
}
break;
default:
records.push(record);
break;
}
}
return JSON.stringify({
tldrawFileFormatVersion: LATEST_TLDRAW_FILE_FORMAT_VERSION,
schema: editor.store.schema.serialize(),
records: pruneUnusedAssets(records)
});
}
async function serializeTldrawJsonBlob(editor) {
return new Blob([await serializeTldrawJson(editor)], { type: TLDRAW_FILE_MIMETYPE });
}
async function parseAndLoadDocument(editor, document, msg, addToast, onV1FileLoad, forceDarkMode) {
const parseFileResult = parseTldrawJsonFile({
schema: editor.store.schema,
json: document
});
if (!parseFileResult.ok) {
let description;
switch (parseFileResult.error.type) {
case "notATldrawFile":
editor.annotateError(parseFileResult.error.cause, {
origin: "file-system.open.parse",
willCrashApp: false,
tags: { parseErrorType: parseFileResult.error.type }
});
reportError(parseFileResult.error.cause);
description = msg("file-system.file-open-error.not-a-tldraw-file");
break;
case "fileFormatVersionTooNew":
description = msg("file-system.file-open-error.file-format-version-too-new");
break;
case "migrationFailed":
if (parseFileResult.error.reason === MigrationFailureReason.TargetVersionTooNew) {
description = msg("file-system.file-open-error.file-format-version-too-new");
} else {
description = msg("file-system.file-open-error.generic-corrupted-file");
}
break;
case "invalidRecords":
editor.annotateError(parseFileResult.error.cause, {
origin: "file-system.open.parse",
willCrashApp: false,
tags: { parseErrorType: parseFileResult.error.type }
});
reportError(parseFileResult.error.cause);
description = msg("file-system.file-open-error.generic-corrupted-file");
break;
case "v1File": {
buildFromV1Document(editor, parseFileResult.error.data.document);
onV1FileLoad?.();
return;
}
default:
exhaustiveSwitchError(parseFileResult.error, "type");
}
addToast({
title: msg("file-system.file-open-error.title"),
description,
severity: "error"
});
return;
}
transact(() => {
editor.loadSnapshot(parseFileResult.value.getStoreSnapshot());
editor.clearHistory();
const bounds = editor.getCurrentPageBounds();
if (bounds) {
editor.zoomToBounds(bounds, { targetZoom: 1, immediate: true });
}
});
if (forceDarkMode) editor.user.updateUserPreferences({ colorScheme: "dark" });
}
export {
TLDRAW_FILE_EXTENSION,
TLDRAW_FILE_MIMETYPE,
isV1File,
parseAndLoadDocument,
parseTldrawJsonFile,
serializeTldrawJson,
serializeTldrawJsonBlob
};
//# sourceMappingURL=file.mjs.map