feat(plugins): csv and json import
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
06a59e1b5c
commit
a540c7d34d
@@ -0,0 +1,82 @@
|
||||
export type CsvDelimiter = "," | ";" | "\t";
|
||||
|
||||
function detectDelimiter(text: string): CsvDelimiter {
|
||||
const counts: Record<CsvDelimiter, number> = { ",": 0, ";": 0, "\t": 0 };
|
||||
let inQuotes = false;
|
||||
for (const ch of text) {
|
||||
if (ch === '"') inQuotes = !inQuotes;
|
||||
else if (!inQuotes) {
|
||||
if (ch === "\n" || ch === "\r") break;
|
||||
if (ch === "," || ch === ";" || ch === "\t") counts[ch]++;
|
||||
}
|
||||
}
|
||||
let best: CsvDelimiter = ",";
|
||||
for (const d of [";", "\t"] as const) if (counts[d] > counts[best]) best = d;
|
||||
return best;
|
||||
}
|
||||
|
||||
/** RFC 4180 parser. Strips a BOM, auto-detects the delimiter from the header line, skips blank lines. */
|
||||
export function parseCsv(text: string, opts: { delimiter?: CsvDelimiter } = {}): string[][] {
|
||||
if (text.charCodeAt(0) === 0xfeff) text = text.slice(1);
|
||||
const delim = opts.delimiter ?? detectDelimiter(text);
|
||||
const rows: string[][] = [];
|
||||
let row: string[] = [];
|
||||
let field = "";
|
||||
let quoted = false; // current field was quoted
|
||||
let inQuotes = false;
|
||||
let pending = false; // something has been read for the current row
|
||||
|
||||
const endField = () => {
|
||||
row.push(field);
|
||||
field = "";
|
||||
};
|
||||
const endRow = () => {
|
||||
const blank = row.length === 0 && field === "" && !quoted;
|
||||
if (!blank) {
|
||||
endField();
|
||||
rows.push(row);
|
||||
}
|
||||
row = [];
|
||||
field = "";
|
||||
quoted = false;
|
||||
pending = false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const ch = text[i]!;
|
||||
if (inQuotes) {
|
||||
if (ch === '"') {
|
||||
if (text[i + 1] === '"') {
|
||||
field += '"';
|
||||
i++;
|
||||
} else inQuotes = false;
|
||||
} else field += ch;
|
||||
} else if (ch === '"' && field === "" && !quoted) {
|
||||
inQuotes = true;
|
||||
quoted = true;
|
||||
pending = true;
|
||||
} else if (ch === delim) {
|
||||
endField();
|
||||
quoted = false;
|
||||
pending = true;
|
||||
} else if (ch === "\r" || ch === "\n") {
|
||||
if (ch === "\r" && text[i + 1] === "\n") i++;
|
||||
endRow();
|
||||
} else {
|
||||
field += ch;
|
||||
pending = true;
|
||||
}
|
||||
}
|
||||
if (pending || field !== "" || row.length > 0) endRow();
|
||||
return rows;
|
||||
}
|
||||
|
||||
function quoteCell(s: string, single: boolean): string {
|
||||
if (s === "" ? single : /[",;\t\r\n]/.test(s) || s !== s.trim()) return `"${s.replaceAll('"', '""')}"`;
|
||||
return s;
|
||||
}
|
||||
|
||||
/** Serialises with `,` and `\r\n`, quoting only where needed. */
|
||||
export function toCsv(rows: string[][]): string {
|
||||
return rows.map((r) => r.map((c) => quoteCell(c, r.length === 1)).join(",") + "\r\n").join("");
|
||||
}
|
||||
Reference in New Issue
Block a user