import * as XLSX from 'xlsx'; import type { ImportedDataset } from '../domain/dataset'; import type { DiscardedImportContent, ImportDocument } from '../domain/import-intermediate'; import type { ImportSource } from '../domain/import-source'; import { rowsToCsv } from '../exports/csv-export'; import { clean } from '../shared/text'; import { escapeHtmlText, isSafeLinkTarget } from './html-extraction'; import { createImportDocument, createImportTable, importDocumentToDataset } from './intermediate'; /** * Excel fuehrt Verweisziele nicht im Zelltext, sondern als eigene Eigenschaft * der Zelle - `sheet_to_json` liefert sie deshalb nicht mit, und die * Belegstellen der Werkzeuge gingen beim Import verloren. * * Zellen mit sicherem Ziel werden als Anker geschrieben, damit der Verweis im * Feldwert ankommt und in der Darstellung anklickbar bleibt. Das Format kennt * hoechstens ein Ziel je Zelle; mehrere Belegstellen in einer Zelle bleiben * daher unverlinkter Text. */ function withCellLinks(sheet: XLSX.WorkSheet, rows: unknown[][], sourceRowIndexes: number[]): unknown[][] { return rows.map((row, rowIndex) => row.map((value, columnIndex) => { const reference = XLSX.utils.encode_cell({ r: sourceRowIndexes[rowIndex] ?? rowIndex, c: columnIndex }); const target = (sheet[reference] as XLSX.CellObject | undefined)?.l?.Target ?? ''; const text = clean(value); if (!text || !isSafeLinkTarget(target)) return value; return `${escapeHtmlText(text)}`; })); } export async function parseExcelDocument(source: ImportSource): Promise { const workbook = XLSX.read(source.bytes, { type: 'array', cellDates: true, cellFormula: false, bookVBA: false }); const visibility = new Map((workbook.Workbook?.Sheets ?? []).map(sheet => [sheet.name, Number(sheet.Hidden ?? 0)])); const discardedContent: DiscardedImportContent[] = []; const tables = workbook.SheetNames.flatMap((name, workbookIndex) => { const sheet = workbook.Sheets[name]; const decodedRows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: false, blankrows: true, dateNF: 'yyyy-mm-dd' }); const rawRows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: true, blankrows: true }); const sourceRowIndexes = decodedRows .map((row, rowIndex) => ({ row, rowIndex })) .filter(item => item.row.some(cell => clean(cell))) .map(item => item.rowIndex); if (!sourceRowIndexes.length) { discardedContent.push({ kind: 'empty-sheet', reason: 'Leere Excel-Sheets enthalten keine importierbaren Tabellenwerte.', source: { fileName: source.metadata.name, tableIndex: workbookIndex } }); return []; } const compactDecodedRows = withCellLinks( sheet, sourceRowIndexes.map(rowIndex => decodedRows[rowIndex] ?? []), sourceRowIndexes ); // Die Rohwerte bleiben unveraendert die Quellwerte der Zellen. const compactRawRows = sourceRowIndexes.map(rowIndex => rawRows[rowIndex] ?? []); return [createImportTable({ sourceFile: source.metadata.name, name, index: workbookIndex, decodedRows: compactDecodedRows, rawRows: compactRawRows, rawText: rowsToCsv(compactDecodedRows), attributes: { workbookIndex, hidden: visibility.get(name) ?? 0 }, positionForCell: (rowIndex, columnIndex) => ({ cellReference: XLSX.utils.encode_cell({ r: sourceRowIndexes[rowIndex] ?? rowIndex, c: columnIndex }) }) })]; }); const rawText = tables.map(table => `# ${table.name}\n${table.rawText}`).join('\n\n'); return createImportDocument({ file: source.metadata, format: 'Excel', rawText, tables, discardedContent }); } export async function importExcel(source: ImportSource): Promise> { return importDocumentToDataset(await parseExcelDocument(source)); }