"use client"; import type { Editor } from "@tiptap/react"; import { type ComponentType, Suspense, useEffect, useMemo, useRef } from "react"; import { cn } from "../lib/utils"; import { retryableLazyEditor } from "./editor-lazy"; import { EditorLoadingPlaceholder } from "./editor-loading"; import { type DocumentEditorPeers, loadDocumentEditorPeers } from "./editor-peers"; import { EditorToolbar } from "./editor-toolbar"; import { htmlToMarkdown, markdownToHtml, normalizeMarkdown, } from "./markdown-conversion"; export interface MarkdownDocumentEditorProps { value?: string; placeholder?: string; readOnly?: boolean; autoFocus?: boolean; className?: string; contentClassName?: string; onChange?: (markdown: string, editor: Editor) => void; onReady?: (editor: Editor) => void; } /** * Builds the local markdown editor against loaded tiptap namespaces. The * peers arrive as an argument so that this module reaches them through * type-only imports, which a bundler erases. */ export function createMarkdownDocumentEditor( peers: DocumentEditorPeers, ): ComponentType { const { EditorContent, useEditor } = peers.react; const StarterKit = peers.starterKit.default; return function MarkdownDocumentEditor({ value = "", placeholder = "Start writing...", readOnly = false, autoFocus = false, className, contentClassName, onChange, onReady, }: MarkdownDocumentEditorProps) { const initialHtml = useMemo(() => markdownToHtml(value), []); const lastAppliedMarkdownRef = useRef(normalizeMarkdown(value)); const editor = useEditor({ extensions: [ StarterKit.configure({ codeBlock: { HTMLAttributes: { class: "hljs", }, }, }), ], content: initialHtml, editable: !readOnly, autofocus: autoFocus, editorProps: { attributes: { class: cn( "prose prose-sm sm:prose-base max-w-none focus:outline-none", "prose-headings:text-foreground prose-p:text-foreground prose-li:text-foreground", "prose-strong:text-foreground prose-code:text-foreground prose-pre:text-foreground", contentClassName, ), "data-placeholder": placeholder, }, }, onUpdate: ({ editor: currentEditor }) => { const nextMarkdown = normalizeMarkdown(htmlToMarkdown(currentEditor.getHTML())); lastAppliedMarkdownRef.current = nextMarkdown; onChange?.(nextMarkdown, currentEditor); }, onCreate: ({ editor: currentEditor }) => { onReady?.(currentEditor); }, }); useEffect(() => { if (editor) { editor.setEditable(!readOnly); } }, [editor, readOnly]); useEffect(() => { if (!editor) { return; } const normalizedValue = normalizeMarkdown(value); if (normalizedValue === lastAppliedMarkdownRef.current) { return; } editor.commands.setContent(markdownToHtml(value), { emitUpdate: false }); lastAppliedMarkdownRef.current = normalizedValue; }, [editor, value]); return (
); }; } const lazyMarkdownDocumentEditor = retryableLazyEditor(async () => createMarkdownDocumentEditor(await loadDocumentEditorPeers()), ); /** Local markdown editor. Loads its tiptap peers on first render. */ export function MarkdownDocumentEditor(props: MarkdownDocumentEditorProps) { const LazyMarkdownDocumentEditor = lazyMarkdownDocumentEditor(); return ( } > ); }