1/** A table preview is bounded independently of the source preview's byte limit. */
2export function filePreviewDelimiter(file: { name: string; mimeType?: string }): "," | "\t" | null {
3 const mime = file.mimeType?.split(";", 1)[0]?.trim().toLowerCase();
4 if (mime === "text/csv") return ",";
5 if (mime === "text/tab-separated-values") return "\t";
6 if (mime && mime !== "text/plain" && mime !== "application/octet-stream") return null;
7 if (/\.csv$/i.test(file.name)) return ",";
8 if (/\.tsv$/i.test(file.name)) return "\t";
9 return null;
10}
11
12/** Preserve quoted delimiters, escaped quotes and multiline cells; raw mode retains all text. */
13export function parseDelimitedPreview(text: string, delimiter: "," | "\t") {
14 const rows: string[][] = [];
15 let row: string[] = [];
16 let cell = "";
17 let quoted = false;
18 let truncated = false;
19 let rowStart = text.charCodeAt(0) === 0xfeff ? 1 : 0;
20 const endCell = () => {
21 if (row.length < 30) row.push(cell);
22 else truncated = true;
23 cell = "";
24 };
25 for (let index = rowStart; index < text.length; index++) {
26 const char = text[index];
27 if (char === '"') {
28 if (quoted && text[index + 1] === '"') {
29 if (cell.length < 2000) cell += '"';
30 else truncated = true;
31 index++;
32 continue;
33 }
34 if (quoted || cell === "") {
35 quoted = !quoted;
36 continue;
37 }
38 }
39 if (!quoted && (char === delimiter || char === "\n" || char === "\r")) {
40 endCell();
41 if (char !== delimiter) {
42 rows.push(row);
43 row = [];
44 if (char === "\r" && text[index + 1] === "\n") index++;
45 rowStart = index + 1;
46 if (rows.length === 100) return { rows, truncated: truncated || index < text.length - 1 };
47 }
48 } else if (cell.length < 2000) cell += char;
49 else truncated = true;
50 }
51 if (rowStart < text.length) {
52 endCell();
53 rows.push(row);
54 }
55 return { rows, truncated: truncated || quoted };
56}