/** * Pure CSV/Blob-download helpers (Sprint 3 Phase C — extraction из DealsView). * * Все функции работают через DOM API (`document.createElement('a')`, click, * URL.createObjectURL). В jsdom-среде Vitest createObjectURL может быть * undefined — функции gracefully no-op'ят, тесты явно мокают через * `Object.defineProperty(URL, 'createObjectURL', ...)`. */ function csvEscape(value: string): string { // CSV-стандарт: значение в кавычках если содержит ; / " / \n; внутри двойные «"». if (value.includes(';') || value.includes('"') || value.includes('\n')) { return '"' + value.replace(/"/g, '""') + '"'; } return value; } export function triggerBlobDownload(blob: Blob, filename: string): void { if (typeof URL.createObjectURL !== 'function') return; const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 0); } export function triggerCsvDownload(csv: string, filename: string): void { // jsdom не реализует URL.createObjectURL; в проде — стандартный browser-flow. if (typeof URL.createObjectURL !== 'function') return; const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(url), 0); } /** * Сборка CSV-строки из массива объектов: header-row + строки + BOM (U+FEFF). * BOM нужен чтобы Excel корректно распознавал UTF-8. */ export function buildCsvString(headers: string[], rows: (string | number)[][]): string { const lines = [headers.join(';'), ...rows.map((row) => row.map((v) => csvEscape(String(v))).join(';'))]; // String.fromCharCode(0xfeff) вместо литерального BOM — иначе ESLint // no-irregular-whitespace. return String.fromCharCode(0xfeff) + lines.join('\r\n'); }