| author | |
| committer | |
| log | f1add18ae1560f509405a5eece07f3cfb1cc850f |
| tree | 1c125133b75f8bab45be4196873ec0562a4c3c5b |
| parent | 21e2b7de0062a3c1dca7209ca48b736dd9a36c84 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Simone <185146821+Lucenx9@users.noreply.github.com>2 files changed, 33 insertions(+), 2 deletions(-)
packages/shared/src/delimitedPreview.test.ts+29| ... | @@ -21,6 +21,35 @@ describe("delimited file previews", () => { | ... | @@ -21,6 +21,35 @@ describe("delimited file previews", () => { |
| 21 | ["", "three"], | 21 | ["", "three"], |
| 22 | ]); | 22 | ]); |
| 23 | }); | 23 | }); |
| 24 | describe.each([",", "\t"] as const)("with delimiter %j", (delimiter) => { | ||
| 25 | it("preserves a final quoted empty record without requiring a line ending", () => { | ||
| 26 | for (const ending of ["", "\n", "\r\n"]) { | ||
| 27 | expect(parseDelimitedPreview(`name\r\nAlice\r\n""${ending}`, delimiter)).toEqual({ | ||
| 28 | rows: [["name"], ["Alice"], [""]], | ||
| 29 | truncated: false, | ||
| 30 | }); | ||
| 31 | } | ||
| 32 | expect(parseDelimitedPreview('""', delimiter)).toEqual({ | ||
| 33 | rows: [[""]], | ||
| 34 | truncated: false, | ||
| 35 | }); | ||
| 36 | }); | ||
| 37 | it("does not invent records for empty input or a trailing line ending", () => { | ||
| 38 | for (const prefix of ["", "\ufeff"]) { | ||
| 39 | expect(parseDelimitedPreview(prefix, delimiter).rows).toEqual([]); | ||
| 40 | expect(parseDelimitedPreview(`${prefix}name\r\n`, delimiter).rows).toEqual([["name"]]); | ||
| 41 | expect(parseDelimitedPreview(`${prefix}""`, delimiter).rows).toEqual([[""]]); | ||
| 42 | } | ||
| 43 | }); | ||
| 44 | it("keeps a quoted empty record at the row limit", () => { | ||
| 45 | const text = `${"name\n".repeat(99)}""`; | ||
| 46 | const preview = parseDelimitedPreview(text, delimiter); | ||
| 47 | expect(preview.rows).toHaveLength(100); | ||
| 48 | expect(preview.rows.at(-1)).toEqual([""]); | ||
| 49 | expect(preview.truncated).toBe(false); | ||
| 50 | expect(parseDelimitedPreview(`${text}\nextra`, delimiter).truncated).toBe(true); | ||
| 51 | }); | ||
| 52 | }); | ||
| 24 | it("bounds rows, columns and cell length and reports partial content", () => { | 53 | it("bounds rows, columns and cell length and reports partial content", () => { |
| 25 | const table = parseDelimitedPreview( | 54 | const table = parseDelimitedPreview( |
| 26 | Array.from({ length: 101 }, () => | 55 | Array.from({ length: 101 }, () => |
packages/shared/src/delimitedPreview.ts+4-2| ... | @@ -16,12 +16,13 @@ export function parseDelimitedPreview(text: string, delimiter: "," | "\t") { | ... | @@ -16,12 +16,13 @@ export function parseDelimitedPreview(text: string, delimiter: "," | "\t") { |
| 16 | let cell = ""; | 16 | let cell = ""; |
| 17 | let quoted = false; | 17 | let quoted = false; |
| 18 | let truncated = false; | 18 | let truncated = false; |
| 19 | let rowStart = text.charCodeAt(0) === 0xfeff ? 1 : 0; | ||
| 19 | const endCell = () => { | 20 | const endCell = () => { |
| 20 | if (row.length < 30) row.push(cell); | 21 | if (row.length < 30) row.push(cell); |
| 21 | else truncated = true; | 22 | else truncated = true; |
| 22 | cell = ""; | 23 | cell = ""; |
| 23 | }; | 24 | }; |
| 24 | for (let index = text.charCodeAt(0) === 0xfeff ? 1 : 0; index < text.length; index++) { | 25 | for (let index = rowStart; index < text.length; index++) { |
| 25 | const char = text[index]; | 26 | const char = text[index]; |
| 26 | if (char === '"') { | 27 | if (char === '"') { |
| 27 | if (quoted && text[index + 1] === '"') { | 28 | if (quoted && text[index + 1] === '"') { |
| ... | @@ -41,12 +42,13 @@ export function parseDelimitedPreview(text: string, delimiter: "," | "\t") { | ... | @@ -41,12 +42,13 @@ export function parseDelimitedPreview(text: string, delimiter: "," | "\t") { |
| 41 | rows.push(row); | 42 | rows.push(row); |
| 42 | row = []; | 43 | row = []; |
| 43 | if (char === "\r" && text[index + 1] === "\n") index++; | 44 | if (char === "\r" && text[index + 1] === "\n") index++; |
| 45 | rowStart = index + 1; | ||
| 44 | if (rows.length === 100) return { rows, truncated: truncated || index < text.length - 1 }; | 46 | if (rows.length === 100) return { rows, truncated: truncated || index < text.length - 1 }; |
| 45 | } | 47 | } |
| 46 | } else if (cell.length < 2000) cell += char; | 48 | } else if (cell.length < 2000) cell += char; |
| 47 | else truncated = true; | 49 | else truncated = true; |
| 48 | } | 50 | } |
| 49 | if (cell.length || row.length) { | 51 | if (rowStart < text.length) { |
| 50 | endCell(); | 52 | endCell(); |
| 51 | rows.push(row); | 53 | rows.push(row); |
| 52 | } | 54 | } |