1import { 2 sanitizeExportFilename, 3 sanitizeSpreadsheetText 4} from './spreadsheet-security.41d5397c2012.js'; 5import { 6 STRUCTURED_SNAPSHOT_FORMAT, 7 STRUCTURED_SNAPSHOT_VERSION 8} from './structured-snapshot.82adae409b7d.js'; 9 10export const CSV_MIME_TYPE = 'text/csv;charset=utf-8'; 11export const CSV_HEADERS = Object.freeze([ 12 'Document Type', 13 'Document Number', 14 'Issue Date', 15 'Due Date', 16 'Currency', 17 'Locale', 18 'Seller Name', 19 'Buyer Name', 20 'Line Number', 21 'Description', 22 'Quantity', 23 'Unit', 24 'Unit Price', 25 'Line Subtotal', 26 'Tax Label', 27 'Tax Rate', 28 'Tax Amount', 29 'Document Subtotal', 30 'Document Discount', 31 'Document Tax', 32 'Document Fees', 33 'Document Total', 34 'Export Format', 35 'Export Version' 36]); 37 38const DECIMAL_PATTERN = /^-?\d+(?:\.\d+)?$/; 39 40function numericCell(value) { 41 const decimal = String(value ?? ''); 42 if (!DECIMAL_PATTERN.test(decimal)) throw new TypeError('CSV decimal cells must use canonical decimal values.'); 43 return Object.freeze({ decimal }); 44} 45 46function serializeCell(value) { 47 if (value && typeof value === 'object' && typeof value.decimal === 'string') return value.decimal; 48 if (typeof value === 'number') { 49 if (!Number.isFinite(value)) throw new TypeError('CSV numeric cells must be finite.'); 50 return String(value); 51 } 52 const safe = sanitizeSpreadsheetText(value); 53 return `"${safe.replace(/"/g, '""').replace(/\n/g, '\r\n')}"`; 54} 55 56function assertSnapshot(snapshot) { 57 if (snapshot?.format !== STRUCTURED_SNAPSHOT_FORMAT || snapshot?.version !== STRUCTURED_SNAPSHOT_VERSION) { 58 throw new TypeError('Unsupported structured snapshot for CSV export.'); 59 } 60 if (!Array.isArray(snapshot.items) || snapshot.items.length === 0) { 61 throw new TypeError('CSV export requires at least one line item.'); 62 } 63} 64 65export function buildLineItemsCsv(snapshot) { 66 assertSnapshot(snapshot); 67 const rows = snapshot.items.map((item) => [ 68 snapshot.document.label, 69 snapshot.document.number, 70 snapshot.document.issueDate, 71 snapshot.document.dueDate, 72 snapshot.document.currency, 73 snapshot.document.locale, 74 snapshot.seller.name, 75 snapshot.buyer.name, 76 item.position, 77 item.description, 78 numericCell(item.quantity), 79 item.unit, 80 numericCell(item.unitPrice), 81 numericCell(item.lineSubtotal), 82 item.taxLabel, 83 numericCell(item.taxRate), 84 numericCell(item.taxAmount), 85 numericCell(snapshot.totals.subtotal), 86 numericCell(snapshot.totals.discountTotal), 87 numericCell(snapshot.totals.taxTotal), 88 numericCell(snapshot.totals.feesTotal), 89 numericCell(snapshot.totals.grandTotal), 90 snapshot.format, 91 snapshot.version 92 ]); 93 return `\uFEFF${[CSV_HEADERS, ...rows].map((row) => row.map(serializeCell).join(',')).join('\r\n')}\r\n`; 94} 95 96export function buildCsvFilename(snapshot) { 97 assertSnapshot(snapshot); 98 const base = sanitizeExportFilename(snapshot.document.number || snapshot.document.type, { 99 fallback: 'invoicecraftly-document' 100 }); 101 return `${base}-line-items.csv`; 102}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.