|
1 | | -import { getDelimiter } from './delimiter' |
2 | | -import { getNewline } from './newline' |
3 | | -import { getQuote } from './quote' |
4 | | - |
5 | | -export interface ChunkResult { |
6 | | - data: string[] |
7 | | - metadata: { |
8 | | - byteCount: number |
9 | | - offset: number |
10 | | - delimiter: string |
11 | | - newline: string |
12 | | - quote: string |
13 | | - } |
14 | | -} |
| 1 | +import { DefaultDelimiter } from './constants' |
| 2 | +import { guessDelimiter, validateDelimiter } from './delimiter' |
| 3 | +import { guessLineEndings, validateNewline } from './newline' |
| 4 | +import { parse } from './parser' |
| 5 | +import { validateQuoteChar } from './quoteChar' |
| 6 | +import type { ParseResult } from './types' |
| 7 | +import { testEmptyLine } from './utils' |
15 | 8 |
|
16 | 9 | /** |
17 | 10 | * Parses a chunk of bytes into CSV data. |
18 | 11 | * @param options Options for parsing the chunk. |
19 | 12 | * @param options.bytes The chunk of bytes to parse. |
20 | 13 | * @param options.delimiter The delimiter used in the CSV data. Defaults to ','. |
21 | 14 | * @param options.newline The newline used in the CSV data. Defaults to '\n'. |
22 | | - * @param options.quote The quote character used in the CSV data. Defaults to '"'. |
| 15 | + * @param options.quoteChar The quote character used in the CSV data. Defaults to '"'. |
| 16 | + * @param options.comments The comment character or boolean to indicate comments |
| 17 | + * @param options.delimitersToGuess The list of delimiters to guess from |
| 18 | + * @param options.skipEmptyLines Whether to skip empty lines, if so, whether 'greedy' or not. Defaults to false. |
| 19 | + * @param options.ignoreLastRow Whether to ignore the last row. Defaults to false. |
23 | 20 | * @yields Parsed data and metadata. |
24 | 21 | * @returns A generator yielding parsed data and metadata. |
25 | 22 | */ |
26 | 23 | export function* parseChunk({ |
27 | 24 | bytes, |
28 | 25 | delimiter, |
29 | 26 | newline, |
30 | | - quote, |
| 27 | + quoteChar, |
| 28 | + comments, |
| 29 | + delimitersToGuess, |
| 30 | + skipEmptyLines, |
| 31 | + ignoreLastRow, |
31 | 32 | }: { |
32 | 33 | bytes: Uint8Array |
33 | 34 | delimiter?: string |
34 | 35 | newline?: string |
35 | | - quote?: string |
36 | | -}): Generator<ChunkResult, void, unknown> { |
37 | | - delimiter ??= getDelimiter() |
38 | | - newline ??= getNewline() |
39 | | - quote ??= getQuote() |
40 | | - |
41 | | - // TODO(SL): reuse decoder? |
| 36 | + quoteChar?: string |
| 37 | + comments?: boolean | string |
| 38 | + delimitersToGuess?: string[] |
| 39 | + skipEmptyLines?: boolean | 'greedy' |
| 40 | + ignoreLastRow?: boolean |
| 41 | +}): Generator<ParseResult, void, unknown> { |
42 | 42 | const decoder = new TextDecoder('utf-8') |
| 43 | + const input = decoder.decode(bytes) |
| 44 | + |
| 45 | + skipEmptyLines ??= false |
| 46 | + quoteChar = validateQuoteChar(quoteChar) |
| 47 | + newline = validateNewline(newline) ?? guessLineEndings(input, quoteChar) |
| 48 | + |
| 49 | + let delimiterError = false |
| 50 | + delimiter = validateDelimiter(delimiter) |
| 51 | + if (!delimiter) { |
| 52 | + const delimGuess = guessDelimiter(input, newline, skipEmptyLines, comments, delimitersToGuess) |
| 53 | + if (delimGuess.successful) |
| 54 | + delimiter = delimGuess.bestDelimiter |
| 55 | + else { |
| 56 | + delimiterError = true // add error after first row parsing |
| 57 | + delimiter = DefaultDelimiter |
| 58 | + } |
| 59 | + } |
| 60 | + |
| 61 | + for (const result of parse(input, { |
| 62 | + delimiter, |
| 63 | + newline, |
| 64 | + quoteChar, |
| 65 | + ignoreLastRow, |
| 66 | + comments, |
| 67 | + // TODO(SL): add escapeChar? |
| 68 | + })) { |
| 69 | + if (delimiterError) { |
| 70 | + result.errors.push({ |
| 71 | + type: 'Delimiter', |
| 72 | + code: 'UndetectableDelimiter', |
| 73 | + message: 'Unable to auto-detect delimiting character; defaulted to \'' + DefaultDelimiter + '\'', |
| 74 | + }) |
| 75 | + delimiterError = false |
| 76 | + } |
43 | 77 |
|
44 | | - const text = decoder.decode(bytes) |
| 78 | + if (skipEmptyLines && testEmptyLine(result.row, skipEmptyLines)) { |
| 79 | + // TODO(SL) accumulate the byte count of removed lines |
| 80 | + continue |
| 81 | + } |
45 | 82 |
|
46 | | - yield { |
47 | | - data: [text], |
48 | | - metadata: { |
49 | | - byteCount: bytes.length, |
50 | | - offset: 0, |
51 | | - delimiter, |
52 | | - newline, |
53 | | - quote, |
54 | | - }, |
| 83 | + yield result |
55 | 84 | } |
56 | 85 | } |
0 commit comments