/** * High-level import/export operations. Each one is atomic: on any failure it * throws and leaves the document untouched (the UI catches and shows a toast). */ import type { DocumentStore } from '../store/document'; import { downloadBlob, downloadText } from './download'; import { serializeDocument, parseDocument, reidSheets } from './json'; import { parseCsv, sniffDelimiter, matrixToCells, tableToCsv, type Delimiter, type CsvExportOptions, } from './csv'; import { documentToXlsx, xlsxToSheets, importedToSheet } from './xlsx'; export function readAsText(file: File): Promise { return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onerror = () => reject(new Error('Datei konnte nicht gelesen werden.')); fr.onload = () => resolve(String(fr.result)); fr.readAsText(file); }); } export function readAsArrayBuffer(file: File): Promise { return new Promise((resolve, reject) => { const fr = new FileReader(); fr.onerror = () => reject(new Error('Datei konnte nicht gelesen werden.')); fr.onload = () => resolve(fr.result as ArrayBuffer); fr.readAsArrayBuffer(file); }); } export function extensionOf(name: string): string { const i = name.lastIndexOf('.'); return i >= 0 ? name.slice(i + 1).toLowerCase() : ''; } /** Yield to the event loop so a large parse doesn't freeze the UI. */ const breathe = () => new Promise((r) => setTimeout(r, 0)); // ---- export --------------------------------------------------------- /** A document title as a file name (without extension). */ export function fileBaseName(title: string): string { return title.replace(/[\\/:*?"<>|\u0000-\u001f]+/g, '-').trim() || 'dokument'; } export function exportJson(store: DocumentStore, name = 'dokument'): void { downloadText(`${fileBaseName(name)}.json`, serializeDocument(store.snapshot()), 'application/json'); } /** Save as the native `.num` file (the JSON document, zipped -- see num.ts). * The zip code loads on first use, like the xlsx support. */ export async function exportNum(store: DocumentStore, name = 'dokument'): Promise { const { packNum, NUM_MIMETYPE } = await import('./num'); const bytes = packNum(serializeDocument(store.snapshot())); downloadBlob(`${fileBaseName(name)}.num`, new Blob([bytes], { type: NUM_MIMETYPE })); } /** The document JSON inside a `.num` file -- then imported like a `.json`. */ export async function readNumFile(buffer: ArrayBuffer): Promise { const { unpackNum } = await import('./num'); return unpackNum(new Uint8Array(buffer)); } export function exportActiveTableCsv(store: DocumentStore, opts: CsvExportOptions): void { const table = store.activeTable.value; if (!table) throw new Error('Keine Tabelle ausgewählt.'); downloadText(`${table.name}.csv`, tableToCsv(store, table.id, opts), 'text/csv;charset=utf-8'); } export async function exportXlsx(store: DocumentStore, name = 'arbeitsmappe'): Promise { downloadBlob(`${fileBaseName(name)}.xlsx`, await documentToXlsx(store)); } // ---- import: JSON ------------------------------------------------ export function importJsonReplace(store: DocumentStore, text: string): void { store.replaceDocument(parseDocument(text)); // throws JsonImportError on bad schema } export function importJsonAppend(store: DocumentStore, text: string): void { store.appendSheets(reidSheets(parseDocument(text))); } // ---- import: CSV ----------------------------------------------- export interface CsvImportConfig { mode: 'new-table' | 'replace-active'; delimiter: Delimiter | 'auto'; detectNumbers: boolean; name?: string; } export async function importCsv( store: DocumentStore, text: string, cfg: CsvImportConfig, ): Promise { const delimiter: Delimiter = cfg.delimiter === 'auto' ? sniffDelimiter(text) : cfg.delimiter; const matrix = parseCsv(text, delimiter); if (matrix.length > 2000) await breathe(); const { cells, rows, cols } = matrixToCells(matrix, cfg.detectNumbers); if (cfg.mode === 'replace-active') { const table = store.activeTable.value; if (!table) throw new Error('Keine Zieltabelle ausgewählt.'); store.replaceTableCells(table.id, cells, Math.max(rows, 12), Math.max(cols, 6)); } else { store.addTableFromCells( store.doc.activeSheetId, cfg.name?.trim() || 'Import', cells, Math.max(rows, 12), Math.max(cols, 6), ); } } // ---- import: XLSX -------------------------------------------- export async function importXlsxFile(store: DocumentStore, buffer: ArrayBuffer): Promise { const imported = await xlsxToSheets(buffer); if (imported.length === 0) throw new Error('Die Arbeitsmappe enthält keine Tabellen.'); await breathe(); store.appendSheets(imported.map((s, i) => importedToSheet(s, i))); }