Document import
Load a Word, OpenDocument, HTML, Markdown, or text file through the same HTML engine as paste.
import { useRef, useState } from "react";
import { BlockEditor, DocumentImportError, type BlockEditorHandle, type FidelityReport } from "genesis-editor";
import { ImportReport } from "@/components/import-report";
export default function DocumentImportExample() {
const editor = useRef<BlockEditorHandle>(null);
const input = useRef<HTMLInputElement>(null);
const [report, setReport] = useState<FidelityReport | null>(null);
const [error, setError] = useState("");
const load = async (file: File | undefined) => {
if (!file || !editor.current) return;
setError("");
try {
setReport(await editor.current.importFile(file));
} catch (caught) {
setReport(null);
setError(caught instanceof DocumentImportError ? caught.message : String(caught));
}
};
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2 px-15 text-sm">
<button className="rounded-md border px-2.5 py-1 hover:bg-muted" onClick={() => input.current?.click()}>
Import file
</button>
<input
ref={input}
type="file"
data-testid="document-import-example-input"
accept=".docx,.odt,.rtf,.html,.htm,.md,.markdown,.txt,.pages"
hidden
onChange={(event) => {
const file = event.target.files?.[0];
event.target.value = "";
void load(file);
}}
/>
</div>
{(report || error) && (
<div className="mx-15 rounded-lg border bg-muted/30 p-3" data-testid="document-import-status" aria-live="polite">
{error && (
<p className="text-sm text-destructive" data-testid="document-import-error">
{error}
</p>
)}
{report && <ImportReport report={report} testId="document-import" />}
</div>
)}
<BlockEditor
ref={editor}
format="html"
defaultValue="<h2>Import a document</h2><p>Word, OpenDocument, HTML, Markdown, or plain text. Pages files ask you to export as Word first.</p>"
onImportError={(err) => setError(err.message)}
/>
</div>
);
}
Files#
importFile(file) replaces the document. Slash menu Import document (/import) opens a file picker. Headings, lists, tables, images, links, colours, and font sizes go through parseHTML, so re-hosting and the fidelity report still apply.
- Word (.docx) and Google Docs downloads: unpacked and walked to HTML with inline color and size, then
parseHTML. Mammoth (BSD-2-Clause) is the fallback when that walk is empty. Font family is not copied. - OpenDocument (.odt): unzipped with fflate (MIT) and mapped to HTML.
- RTF, HTML, Markdown, plain text where the markup is simple enough to map. Markdown uses marked (MIT), also lazy-loaded.
- Apple Pages is detected and refused: export as Word and import that. There isn't a reliable permissive converter for .pages.
Google Docs paste (HTML with docs-internal-guid) already uses this engine; a downloaded .docx is the file path.
const editor = useRef<BlockEditorHandle>(null);
const report = await editor.current.importFile(file);
// report.score, report.blocks, report.warningsGoogle Drive#
genesis never talks to Google and ships no API keys. If your app already has a picker, pass googleDrive.pickFile and a second slash item appears.
<BlockEditor
googleDrive={{
pickFile: async () => myPicker(), // File you already have; genesis does not do OAuth
}}
onImportError={(error) => console.warn(error.code, error.message)}
/>marked, fflate, and mammoth stay out of the initial editor chunk. They load the first time you import a matching file. fflate and marked are MIT; mammoth is BSD-2-Clause.