102 lines
4.0 KiB
TypeScript
102 lines
4.0 KiB
TypeScript
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 `<a href="${escapeHtmlText(target.trim())}">${escapeHtmlText(text)}</a>`;
|
|
}));
|
|
}
|
|
|
|
export async function parseExcelDocument(source: ImportSource): Promise<ImportDocument> {
|
|
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<unknown[]>(sheet, {
|
|
header: 1,
|
|
defval: '',
|
|
raw: false,
|
|
blankrows: true,
|
|
dateNF: 'yyyy-mm-dd'
|
|
});
|
|
const rawRows = XLSX.utils.sheet_to_json<unknown[]>(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<Omit<ImportedDataset, 'profileId' | 'scanTool'>> {
|
|
return importDocumentToDataset(await parseExcelDocument(source));
|
|
}
|