diff --git a/src/components/EditorView.tsx b/src/components/EditorView.tsx index e98d81b..ec03970 100644 --- a/src/components/EditorView.tsx +++ b/src/components/EditorView.tsx @@ -274,6 +274,9 @@ export function EditorView() { content={chapter.content} onChange={(content) => setChapterContent(chapter.id, content)} onReady={setEditor} + onContentError={() => + setNotice("This chapter contains content Margin couldn't fully read; editing may drop the unrecognized parts.") + } /> diff --git a/src/editor/Editor.tsx b/src/editor/Editor.tsx index 972f96f..0a03738 100644 --- a/src/editor/Editor.tsx +++ b/src/editor/Editor.tsx @@ -10,11 +10,14 @@ interface EditorProps { content: JSONContent; onChange: (content: JSONContent) => void; onReady: (editor: TiptapEditor | null) => void; + onContentError: (error: Error) => void; } -export function Editor({ bookId, chapterId, content, onChange, onReady }: EditorProps) { +export function Editor({ bookId, chapterId, content, onChange, onReady, onContentError }: EditorProps) { const onChangeRef = useRef(onChange); onChangeRef.current = onChange; + const onContentErrorRef = useRef(onContentError); + onContentErrorRef.current = onContentError; const latest = useRef(null); @@ -22,7 +25,9 @@ export function Editor({ bookId, chapterId, content, onChange, onReady }: Editor extensions: editorExtensions, content, immediatelyRender: false, + enableContentCheck: true, editorProps: { attributes: { class: "prose" } }, + onContentError: ({ error }) => onContentErrorRef.current(error), onUpdate: ({ editor }) => onChangeRef.current(editor.getJSON()), }); diff --git a/src/library.ts b/src/library.ts index cdd84ef..e3a145e 100644 --- a/src/library.ts +++ b/src/library.ts @@ -1,6 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; import { isDesktop } from "./ipc"; -import { type Book, createBook, starterBook } from "./model/book"; +import { type Book, SCHEMA_VERSION, createBook, schemaVersion, starterBook } from "./model/book"; export interface BookSummary { id: string; @@ -16,11 +16,16 @@ export async function listBooks(): Promise { export async function loadBook(id: string): Promise { const contents = await invoke("load_book", { id }); + let book: Book; try { - return JSON.parse(contents) as Book; + book = JSON.parse(contents) as Book; } catch { throw new Error("the book file is corrupt or unreadable"); } + if (schemaVersion(book.schema) > SCHEMA_VERSION) { + throw new Error("this book was made with a newer version of Margin; update the app to open it"); + } + return book; } export async function saveBook(book: Book): Promise { diff --git a/src/model/book.ts b/src/model/book.ts index dc8017e..4e78be3 100644 --- a/src/model/book.ts +++ b/src/model/book.ts @@ -115,6 +115,14 @@ export interface Book { chapters: Chapter[]; } +export const SCHEMA_VERSION = 1; + +export function schemaVersion(schema: unknown): number { + if (typeof schema !== "string") return 0; + const match = /^margin\/(\d+)$/.exec(schema); + return match ? Number(match[1]) : 0; +} + function paragraph(text: string): JSONContent { return { type: "paragraph", content: [{ type: "text", text }] }; }