UNPKG

@loaders.gl/csv

Version:

Framework-independent loader for CSV and DSV table formats

4 lines 77.3 kB
{ "version": 3, "sources": ["index.js", "csv-loader.js", "papaparse/papaparse.js", "papaparse/async-iterator-streamer.js", "lib/encoders/encode-csv.js", "csv-writer.js"], "sourcesContent": ["// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nexport { CSVLoader } from \"./csv-loader.js\";\nexport { CSVWriter } from \"./csv-writer.js\";\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { AsyncQueue, TableBatchBuilder, convertToArrayRow, convertToObjectRow } from '@loaders.gl/schema';\nimport Papa from \"./papaparse/papaparse.js\";\nimport AsyncIteratorStreamer from \"./papaparse/async-iterator-streamer.js\";\n// __VERSION__ is injected by babel-plugin-version-inline\n// @ts-ignore TS2304: Cannot find name '__VERSION__'.\nconst VERSION = typeof \"4.3.3\" !== 'undefined' ? \"4.3.3\" : 'latest';\nconst DEFAULT_CSV_SHAPE = 'object-row-table';\nexport const CSVLoader = {\n dataType: null,\n batchType: null,\n id: 'csv',\n module: 'csv',\n name: 'CSV',\n version: VERSION,\n extensions: ['csv', 'tsv', 'dsv'],\n mimeTypes: ['text/csv', 'text/tab-separated-values', 'text/dsv'],\n category: 'table',\n parse: async (arrayBuffer, options) => parseCSV(new TextDecoder().decode(arrayBuffer), options),\n parseText: (text, options) => parseCSV(text, options),\n parseInBatches: parseCSVInBatches,\n // @ts-ignore\n // testText: null,\n options: {\n csv: {\n shape: DEFAULT_CSV_SHAPE, // 'object-row-table'\n optimizeMemoryUsage: false,\n // CSV options\n header: 'auto',\n columnPrefix: 'column',\n // delimiter: auto\n // newline: auto\n quoteChar: '\"',\n escapeChar: '\"',\n dynamicTyping: true,\n comments: false,\n skipEmptyLines: true,\n // transform: null?\n delimitersToGuess: [',', '\\t', '|', ';']\n // fastMode: auto\n }\n }\n};\nasync function parseCSV(csvText, options) {\n // Apps can call the parse method directly, we so apply default options here\n const csvOptions = { ...CSVLoader.options.csv, ...options?.csv };\n const firstRow = readFirstRow(csvText);\n const header = csvOptions.header === 'auto' ? isHeaderRow(firstRow) : Boolean(csvOptions.header);\n const parseWithHeader = header;\n const papaparseConfig = {\n // dynamicTyping: true,\n ...csvOptions,\n header: parseWithHeader,\n download: false, // We handle loading, no need for papaparse to do it for us\n transformHeader: parseWithHeader ? duplicateColumnTransformer() : undefined,\n error: (e) => {\n throw new Error(e);\n }\n };\n const result = Papa.parse(csvText, papaparseConfig);\n const rows = result.data;\n const headerRow = result.meta.fields || generateHeader(csvOptions.columnPrefix, firstRow.length);\n const shape = csvOptions.shape || DEFAULT_CSV_SHAPE;\n switch (shape) {\n case 'object-row-table':\n return {\n shape: 'object-row-table',\n data: rows.map((row) => (Array.isArray(row) ? convertToObjectRow(row, headerRow) : row))\n };\n case 'array-row-table':\n return {\n shape: 'array-row-table',\n data: rows.map((row) => (Array.isArray(row) ? row : convertToArrayRow(row, headerRow)))\n };\n default:\n throw new Error(shape);\n }\n}\n// TODO - support batch size 0 = no batching/single batch?\nfunction parseCSVInBatches(asyncIterator, options) {\n // Papaparse does not support standard batch size handling\n // TODO - investigate papaparse chunks mode\n options = { ...options };\n if (options.batchSize === 'auto') {\n options.batchSize = 4000;\n }\n // Apps can call the parse method directly, we so apply default options here\n const csvOptions = { ...CSVLoader.options.csv, ...options?.csv };\n const asyncQueue = new AsyncQueue();\n let isFirstRow = true;\n let headerRow = null;\n let tableBatchBuilder = null;\n let schema = null;\n const config = {\n // dynamicTyping: true, // Convert numbers and boolean values in rows from strings,\n ...csvOptions,\n header: false, // Unfortunately, header detection is not automatic and does not infer shapes\n download: false, // We handle loading, no need for papaparse to do it for us\n // chunkSize is set to 5MB explicitly (same as Papaparse default) due to a bug where the\n // streaming parser gets stuck if skipEmptyLines and a step callback are both supplied.\n // See https://github.com/mholt/PapaParse/issues/465\n chunkSize: 1024 * 1024 * 5,\n // skipEmptyLines is set to a boolean value if supplied. Greedy is set to true\n // skipEmptyLines is handled manually given two bugs where the streaming parser gets stuck if\n // both of the skipEmptyLines and step callback options are provided:\n // - true doesn't work unless chunkSize is set: https://github.com/mholt/PapaParse/issues/465\n // - greedy doesn't work: https://github.com/mholt/PapaParse/issues/825\n skipEmptyLines: false,\n // step is called on every row\n // eslint-disable-next-line complexity, max-statements\n step(results) {\n let row = results.data;\n if (csvOptions.skipEmptyLines) {\n // Manually reject lines that are empty\n const collapsedRow = row.flat().join('').trim();\n if (collapsedRow === '') {\n return;\n }\n }\n const bytesUsed = results.meta.cursor;\n // Check if we need to save a header row\n if (isFirstRow && !headerRow) {\n // Auto detects or can be forced with csvOptions.header\n const header = csvOptions.header === 'auto' ? isHeaderRow(row) : Boolean(csvOptions.header);\n if (header) {\n headerRow = row.map(duplicateColumnTransformer());\n return;\n }\n }\n // If first data row, we can deduce the schema\n if (isFirstRow) {\n isFirstRow = false;\n if (!headerRow) {\n headerRow = generateHeader(csvOptions.columnPrefix, row.length);\n }\n schema = deduceSchema(row, headerRow);\n }\n if (csvOptions.optimizeMemoryUsage) {\n // A workaround to allocate new strings and don't retain pointers to original strings.\n // https://bugs.chromium.org/p/v8/issues/detail?id=2869\n row = JSON.parse(JSON.stringify(row));\n }\n const shape = csvOptions.shape || DEFAULT_CSV_SHAPE;\n // Add the row\n tableBatchBuilder =\n tableBatchBuilder ||\n new TableBatchBuilder(\n // @ts-expect-error TODO this is not a proper schema\n schema, {\n shape,\n ...options\n });\n try {\n tableBatchBuilder.addRow(row);\n // If a batch has been completed, emit it\n const batch = tableBatchBuilder && tableBatchBuilder.getFullBatch({ bytesUsed });\n if (batch) {\n asyncQueue.enqueue(batch);\n }\n }\n catch (error) {\n asyncQueue.enqueue(error);\n }\n },\n // complete is called when all rows have been read\n complete(results) {\n try {\n const bytesUsed = results.meta.cursor;\n // Ensure any final (partial) batch gets emitted\n const batch = tableBatchBuilder && tableBatchBuilder.getFinalBatch({ bytesUsed });\n if (batch) {\n asyncQueue.enqueue(batch);\n }\n }\n catch (error) {\n asyncQueue.enqueue(error);\n }\n asyncQueue.close();\n }\n };\n Papa.parse(asyncIterator, config, AsyncIteratorStreamer);\n // TODO - Does it matter if we return asyncIterable or asyncIterator\n // return asyncQueue[Symbol.asyncIterator]();\n return asyncQueue;\n}\n/**\n * Checks if a certain row is a header row\n * @param row the row to check\n * @returns true if the row looks like a header\n */\nfunction isHeaderRow(row) {\n return row && row.every((value) => typeof value === 'string');\n}\n/**\n * Reads, parses, and returns the first row of a CSV text\n * @param csvText the csv text to parse\n * @returns the first row\n */\nfunction readFirstRow(csvText) {\n const result = Papa.parse(csvText, {\n dynamicTyping: true,\n preview: 1\n });\n return result.data[0];\n}\n/**\n * Creates a transformer that renames duplicate columns. This is needed as Papaparse doesn't handle\n * duplicate header columns and would use the latest occurrence by default.\n * See the header option in https://www.papaparse.com/docs#config\n * @returns a transform function that returns sanitized names for duplicate fields\n */\nfunction duplicateColumnTransformer() {\n const observedColumns = new Set();\n return (col) => {\n let colName = col;\n let counter = 1;\n while (observedColumns.has(colName)) {\n colName = `${col}.${counter}`;\n counter++;\n }\n observedColumns.add(colName);\n return colName;\n };\n}\n/**\n * Generates the header of a CSV given a prefix and a column count\n * @param columnPrefix the columnPrefix to use\n * @param count the count of column names to generate\n * @returns an array of column names\n */\nfunction generateHeader(columnPrefix, count = 0) {\n const headers = [];\n for (let i = 0; i < count; i++) {\n headers.push(`${columnPrefix}${i + 1}`);\n }\n return headers;\n}\nfunction deduceSchema(row, headerRow) {\n const schema = headerRow ? {} : [];\n for (let i = 0; i < row.length; i++) {\n const columnName = (headerRow && headerRow[i]) || i;\n const value = row[i];\n switch (typeof value) {\n case 'number':\n case 'boolean':\n // TODO - booleans could be handled differently...\n schema[columnName] = { name: String(columnName), index: i, type: Float32Array };\n break;\n case 'string':\n default:\n schema[columnName] = { name: String(columnName), index: i, type: Array };\n // We currently only handle numeric rows\n // TODO we could offer a function to map strings to numbers?\n }\n }\n return schema;\n}\n", "// This is a fork of papaparse\n// https://github.com/mholt/PapaParse\n/* @license\nPapa Parse\nv5.0.0-beta.0\nhttps://github.com/mholt/PapaParse\nLicense: MIT\n*/\n// FORK SUMMARY:\n// - Adopt ES6 exports\n// - Implement new AsyncIteratorStreamer\n// - Remove non Async Iterator streamers (can all be handled by new streamer)\n// - Remove unused Worker support (loaders.gl worker system used instead)\n// - Remove unused jQuery plugin support\n// const defaultConfig: Required<CSVParserConfig> = {\n// dynamicTyping: false,\n// dynamicTypingFunction: undefined!,\n// transform: false\n// };\n/* eslint-disable */\nconst BYTE_ORDER_MARK = '\\ufeff';\nfunction CsvToJson(_input, _config = {}, Streamer = StringStreamer) {\n _config = _config || {};\n var dynamicTyping = _config.dynamicTyping || false;\n if (isFunction(dynamicTyping)) {\n _config.dynamicTypingFunction = dynamicTyping;\n // Will be filled on first row call\n dynamicTyping = {};\n }\n _config.dynamicTyping = dynamicTyping;\n _config.transform = isFunction(_config.transform) ? _config.transform : false;\n var streamer = new Streamer(_config);\n return streamer.stream(_input);\n}\nfunction JsonToCsv(_input, _config) {\n // Default configuration\n /** whether to surround every datum with quotes */\n var _quotes = false;\n /** whether to write headers */\n var _writeHeader = true;\n /** delimiting character(s) */\n var _delimiter = ',';\n /** newline character(s) */\n var _newline = '\\r\\n';\n /** quote character */\n var _quoteChar = '\"';\n /** escaped quote character, either \"\" or <config.escapeChar>\" */\n var _escapedQuote = _quoteChar + _quoteChar;\n /** whether to skip empty lines */\n var _skipEmptyLines = false;\n /** the columns (keys) we expect when we unparse objects */\n var _columns = null;\n unpackConfig();\n var quoteCharRegex = new RegExp(escapeRegExp(_quoteChar), 'g');\n if (typeof _input === 'string')\n _input = JSON.parse(_input);\n if (Array.isArray(_input)) {\n if (!_input.length || Array.isArray(_input[0]))\n return serialize(null, _input, _skipEmptyLines);\n else if (typeof _input[0] === 'object')\n return serialize(_columns || Object.keys(_input[0]), _input, _skipEmptyLines);\n }\n else if (typeof _input === 'object') {\n if (typeof _input.data === 'string')\n _input.data = JSON.parse(_input.data);\n if (Array.isArray(_input.data)) {\n if (!_input.fields)\n _input.fields = _input.meta && _input.meta.fields;\n if (!_input.fields)\n _input.fields = Array.isArray(_input.data[0]) ? _input.fields : Object.keys(_input.data[0]);\n if (!Array.isArray(_input.data[0]) && typeof _input.data[0] !== 'object')\n _input.data = [_input.data]; // handles input like [1,2,3] or ['asdf']\n }\n return serialize(_input.fields || [], _input.data || [], _skipEmptyLines);\n }\n // Default (any valid paths should return before this)\n throw new Error('Unable to serialize unrecognized input');\n function unpackConfig() {\n if (typeof _config !== 'object')\n return;\n if (typeof _config.delimiter === 'string' &&\n !Papa.BAD_DELIMITERS.filter(function (value) {\n return _config.delimiter.indexOf(value) !== -1;\n }).length) {\n _delimiter = _config.delimiter;\n }\n if (typeof _config.quotes === 'boolean' || Array.isArray(_config.quotes))\n _quotes = _config.quotes;\n if (typeof _config.skipEmptyLines === 'boolean' || typeof _config.skipEmptyLines === 'string')\n _skipEmptyLines = _config.skipEmptyLines;\n if (typeof _config.newline === 'string')\n _newline = _config.newline;\n if (typeof _config.quoteChar === 'string')\n _quoteChar = _config.quoteChar;\n if (typeof _config.header === 'boolean')\n _writeHeader = _config.header;\n if (Array.isArray(_config.columns)) {\n if (_config.columns.length === 0)\n throw new Error('Option columns is empty');\n _columns = _config.columns;\n }\n if (_config.escapeChar !== undefined) {\n _escapedQuote = _config.escapeChar + _quoteChar;\n }\n }\n /** The double for loop that iterates the data and writes out a CSV string including header row */\n function serialize(fields, data, skipEmptyLines) {\n var csv = '';\n if (typeof fields === 'string')\n fields = JSON.parse(fields);\n if (typeof data === 'string')\n data = JSON.parse(data);\n var hasHeader = Array.isArray(fields) && fields.length > 0;\n var dataKeyedByField = !Array.isArray(data[0]);\n // If there a header row, write it first\n if (hasHeader && _writeHeader) {\n for (var i = 0; i < fields.length; i++) {\n if (i > 0)\n csv += _delimiter;\n csv += safe(fields[i], i);\n }\n if (data.length > 0)\n csv += _newline;\n }\n // Then write out the data\n for (var row = 0; row < data.length; row++) {\n var maxCol = hasHeader ? fields.length : data[row].length;\n var emptyLine = false;\n var nullLine = hasHeader ? Object.keys(data[row]).length === 0 : data[row].length === 0;\n if (skipEmptyLines && !hasHeader) {\n emptyLine =\n skipEmptyLines === 'greedy'\n ? data[row].join('').trim() === ''\n : data[row].length === 1 && data[row][0].length === 0;\n }\n if (skipEmptyLines === 'greedy' && hasHeader) {\n var line = [];\n for (var c = 0; c < maxCol; c++) {\n var cx = dataKeyedByField ? fields[c] : c;\n line.push(data[row][cx]);\n }\n emptyLine = line.join('').trim() === '';\n }\n if (!emptyLine) {\n for (var col = 0; col < maxCol; col++) {\n if (col > 0 && !nullLine)\n csv += _delimiter;\n var colIdx = hasHeader && dataKeyedByField ? fields[col] : col;\n csv += safe(data[row][colIdx], col);\n }\n if (row < data.length - 1 && (!skipEmptyLines || (maxCol > 0 && !nullLine))) {\n csv += _newline;\n }\n }\n }\n return csv;\n }\n /** Encloses a value around quotes if needed (makes a value safe for CSV insertion) */\n function safe(str, col) {\n if (typeof str === 'undefined' || str === null)\n return '';\n if (str.constructor === Date)\n return JSON.stringify(str).slice(1, 25);\n str = str.toString().replace(quoteCharRegex, _escapedQuote);\n var needsQuotes = (typeof _quotes === 'boolean' && _quotes) ||\n (Array.isArray(_quotes) && _quotes[col]) ||\n hasAny(str, Papa.BAD_DELIMITERS) ||\n str.indexOf(_delimiter) > -1 ||\n str.charAt(0) === ' ' ||\n str.charAt(str.length - 1) === ' ';\n return needsQuotes ? _quoteChar + str + _quoteChar : str;\n }\n function hasAny(str, substrings) {\n for (var i = 0; i < substrings.length; i++)\n if (str.indexOf(substrings[i]) > -1)\n return true;\n return false;\n }\n}\n/** ChunkStreamer is the base prototype for various streamer implementations. */\nclass ChunkStreamer {\n _handle;\n _config;\n _finished = false;\n _completed = false;\n _input = null;\n _baseIndex = 0;\n _partialLine = '';\n _rowCount = 0;\n _start = 0;\n isFirstChunk = true;\n _completeResults = {\n data: [],\n errors: [],\n meta: {}\n };\n constructor(config) {\n // Deep-copy the config so we can edit it\n var configCopy = { ...config };\n // @ts-expect-error\n configCopy.chunkSize = parseInt(configCopy.chunkSize); // parseInt VERY important so we don't concatenate strings!\n if (!config.step && !config.chunk) {\n configCopy.chunkSize = null; // disable Range header if not streaming; bad values break IIS - see issue #196\n }\n this._handle = new ParserHandle(configCopy);\n this._handle.streamer = this;\n this._config = configCopy; // persist the copy to the caller\n }\n parseChunk(chunk, isFakeChunk) {\n // First chunk pre-processing\n if (this.isFirstChunk && isFunction(this._config.beforeFirstChunk)) {\n var modifiedChunk = this._config.beforeFirstChunk(chunk);\n if (modifiedChunk !== undefined)\n chunk = modifiedChunk;\n }\n this.isFirstChunk = false;\n // Rejoin the line we likely just split in two by chunking the file\n var aggregate = this._partialLine + chunk;\n this._partialLine = '';\n var results = this._handle.parse(aggregate, this._baseIndex, !this._finished);\n if (this._handle.paused() || this._handle.aborted())\n return;\n var lastIndex = results.meta.cursor;\n if (!this._finished) {\n this._partialLine = aggregate.substring(lastIndex - this._baseIndex);\n this._baseIndex = lastIndex;\n }\n if (results && results.data)\n this._rowCount += results.data.length;\n var finishedIncludingPreview = this._finished || (this._config.preview && this._rowCount >= this._config.preview);\n if (isFunction(this._config.chunk) && !isFakeChunk) {\n this._config.chunk(results, this._handle);\n if (this._handle.paused() || this._handle.aborted())\n return;\n results = undefined;\n // @ts-expect-error\n this._completeResults = undefined;\n }\n if (!this._config.step && !this._config.chunk) {\n this._completeResults.data = this._completeResults.data.concat(results.data);\n this._completeResults.errors = this._completeResults.errors.concat(results.errors);\n this._completeResults.meta = results.meta;\n }\n if (!this._completed &&\n finishedIncludingPreview &&\n isFunction(this._config.complete) &&\n (!results || !results.meta.aborted)) {\n this._config.complete(this._completeResults, this._input);\n this._completed = true;\n }\n // if (!finishedIncludingPreview && (!results || !results.meta.paused)) this._nextChunk();\n return results;\n }\n _sendError(error) {\n if (isFunction(this._config.error))\n this._config.error(error);\n }\n}\nclass StringStreamer extends ChunkStreamer {\n remaining;\n constructor(config = {}) {\n super(config);\n }\n stream(s) {\n this.remaining = s;\n return this._nextChunk();\n }\n _nextChunk() {\n if (this._finished)\n return;\n var size = this._config.chunkSize;\n var chunk = size ? this.remaining.substr(0, size) : this.remaining;\n this.remaining = size ? this.remaining.substr(size) : '';\n this._finished = !this.remaining;\n return this.parseChunk(chunk);\n }\n}\nconst FLOAT = /^\\s*-?(\\d*\\.?\\d+|\\d+\\.?\\d*)(e[-+]?\\d+)?\\s*$/i;\nconst ISO_DATE = /(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))|(\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d([+-][0-2]\\d:[0-5]\\d|Z))/;\n// Use one ParserHandle per entire CSV file or string\nclass ParserHandle {\n _config;\n /** Number of times step was called (number of rows parsed) */\n _stepCounter = 0;\n /** Number of rows that have been parsed so far */\n _rowCounter = 0;\n /** The input being parsed */\n _input;\n /** The core parser being used */\n _parser;\n /** Whether we are paused or not */\n _paused = false;\n /** Whether the parser has aborted or not */\n _aborted = false;\n /** Temporary state between delimiter detection and processing results */\n _delimiterError = false;\n /** Fields are from the header row of the input, if there is one */\n _fields = [];\n /** The last results returned from the parser */\n _results = {\n data: [],\n errors: [],\n meta: {}\n };\n constructor(_config) {\n // One goal is to minimize the use of regular expressions...\n if (isFunction(_config.step)) {\n var userStep = _config.step;\n _config.step = (results) => {\n this._results = results;\n if (this.needsHeaderRow()) {\n this.processResults();\n }\n // only call user's step function after header row\n else {\n this.processResults();\n // It's possbile that this line was empty and there's no row here after all\n if (!this._results.data || this._results.data.length === 0)\n return;\n this._stepCounter += results.data.length;\n if (_config.preview && this._stepCounter > _config.preview) {\n this._parser.abort();\n }\n else {\n userStep(this._results, this);\n }\n }\n };\n }\n this._config = _config;\n }\n /**\n * Parses input. Most users won't need, and shouldn't mess with, the baseIndex\n * and ignoreLastRow parameters. They are used by streamers (wrapper functions)\n * when an input comes in multiple chunks, like from a file.\n */\n parse(input, baseIndex, ignoreLastRow) {\n var quoteChar = this._config.quoteChar || '\"';\n if (!this._config.newline)\n this._config.newline = guessLineEndings(input, quoteChar);\n this._delimiterError = false;\n if (!this._config.delimiter) {\n var delimGuess = this.guessDelimiter(input, this._config.newline, this._config.skipEmptyLines, this._config.comments, this._config.delimitersToGuess);\n if (delimGuess.successful) {\n this._config.delimiter = delimGuess.bestDelimiter;\n }\n else {\n this._delimiterError = true; // add error after parsing (otherwise it would be overwritten)\n this._config.delimiter = Papa.DefaultDelimiter;\n }\n this._results.meta.delimiter = this._config.delimiter;\n }\n else if (isFunction(this._config.delimiter)) {\n this._config.delimiter = this._config.delimiter(input);\n this._results.meta.delimiter = this._config.delimiter;\n }\n var parserConfig = copy(this._config);\n if (this._config.preview && this._config.header)\n parserConfig.preview++; // to compensate for header row\n this._input = input;\n this._parser = new Parser(parserConfig);\n this._results = this._parser.parse(this._input, baseIndex, ignoreLastRow);\n this.processResults();\n return this._paused ? { meta: { paused: true } } : this._results || { meta: { paused: false } };\n }\n paused() {\n return this._paused;\n }\n pause() {\n this._paused = true;\n this._parser.abort();\n this._input = this._input.substr(this._parser.getCharIndex());\n }\n resume() {\n this._paused = false;\n // @ts-expect-error\n this.streamer.parseChunk(this._input, true);\n }\n aborted() {\n return this._aborted;\n }\n abort() {\n this._aborted = true;\n this._parser.abort();\n this._results.meta.aborted = true;\n if (isFunction(this._config.complete)) {\n this._config.complete(this._results);\n }\n this._input = '';\n }\n testEmptyLine(s) {\n return this._config.skipEmptyLines === 'greedy'\n ? s.join('').trim() === ''\n : s.length === 1 && s[0].length === 0;\n }\n processResults() {\n if (this._results && this._delimiterError) {\n this.addError('Delimiter', 'UndetectableDelimiter', \"Unable to auto-detect delimiting character; defaulted to '\" + Papa.DefaultDelimiter + \"'\");\n this._delimiterError = false;\n }\n if (this._config.skipEmptyLines) {\n for (var i = 0; i < this._results.data.length; i++)\n if (this.testEmptyLine(this._results.data[i]))\n this._results.data.splice(i--, 1);\n }\n if (this.needsHeaderRow()) {\n this.fillHeaderFields();\n }\n return this.applyHeaderAndDynamicTypingAndTransformation();\n }\n needsHeaderRow() {\n return this._config.header && this._fields.length === 0;\n }\n fillHeaderFields() {\n if (!this._results)\n return;\n const addHeder = (header) => {\n if (isFunction(this._config.transformHeader))\n header = this._config.transformHeader(header);\n this._fields.push(header);\n };\n if (Array.isArray(this._results.data[0])) {\n for (var i = 0; this.needsHeaderRow() && i < this._results.data.length; i++)\n this._results.data[i].forEach(addHeder);\n this._results.data.splice(0, 1);\n }\n // if _results.data[0] is not an array, we are in a step where _results.data is the row.\n else {\n this._results.data.forEach(addHeder);\n }\n }\n shouldApplyDynamicTyping(field) {\n // Cache function values to avoid calling it for each row\n if (this._config.dynamicTypingFunction && this._config.dynamicTyping[field] === undefined) {\n this._config.dynamicTyping[field] = this._config.dynamicTypingFunction(field);\n }\n return (this._config.dynamicTyping[field] || this._config.dynamicTyping) === true;\n }\n parseDynamic(field, value) {\n if (this.shouldApplyDynamicTyping(field)) {\n if (value === 'true' || value === 'TRUE')\n return true;\n else if (value === 'false' || value === 'FALSE')\n return false;\n else if (FLOAT.test(value))\n return parseFloat(value);\n else if (ISO_DATE.test(value))\n return new Date(value);\n else\n return value === '' ? null : value;\n }\n return value;\n }\n applyHeaderAndDynamicTypingAndTransformation() {\n if (!this._results ||\n !this._results.data ||\n (!this._config.header && !this._config.dynamicTyping && !this._config.transform)) {\n return this._results;\n }\n var incrementBy = 1;\n if (!this._results.data[0] || Array.isArray(this._results.data[0])) {\n this._results.data = this._results.data.map(this.processRow.bind(this));\n incrementBy = this._results.data.length;\n }\n else {\n // @ts-expect-error\n this._results.data = this.processRow(this._results.data, 0);\n }\n if (this._config.header && this._results.meta)\n this._results.meta.fields = this._fields;\n this._rowCounter += incrementBy;\n return this._results;\n }\n processRow(rowSource, i) {\n var row = this._config.header ? {} : [];\n var j;\n for (j = 0; j < rowSource.length; j++) {\n var field = j;\n var value = rowSource[j];\n if (this._config.header)\n field = j >= this._fields.length ? '__parsed_extra' : this._fields[j];\n if (this._config.transform)\n value = this._config.transform(value, field);\n value = this.parseDynamic(field, value);\n if (field === '__parsed_extra') {\n row[field] = row[field] || [];\n row[field].push(value);\n }\n else\n row[field] = value;\n }\n if (this._config.header) {\n if (j > this._fields.length)\n this.addError('FieldMismatch', 'TooManyFields', 'Too many fields: expected ' + this._fields.length + ' fields but parsed ' + j, this._rowCounter + i);\n else if (j < this._fields.length)\n this.addError('FieldMismatch', 'TooFewFields', 'Too few fields: expected ' + this._fields.length + ' fields but parsed ' + j, this._rowCounter + i);\n }\n return row;\n }\n guessDelimiter(input, newline, skipEmptyLines, comments, delimitersToGuess) {\n var bestDelim, bestDelta, fieldCountPrevRow;\n delimitersToGuess = delimitersToGuess || [',', '\\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP];\n for (var i = 0; i < delimitersToGuess.length; i++) {\n var delim = delimitersToGuess[i];\n var delta = 0, avgFieldCount = 0, emptyLinesCount = 0;\n fieldCountPrevRow = undefined;\n var preview = new Parser({\n comments: comments,\n delimiter: delim,\n newline: newline,\n preview: 10\n }).parse(input);\n for (var j = 0; j < preview.data.length; j++) {\n if (skipEmptyLines && this.testEmptyLine(preview.data[j])) {\n emptyLinesCount++;\n continue;\n }\n var fieldCount = preview.data[j].length;\n avgFieldCount += fieldCount;\n if (typeof fieldCountPrevRow === 'undefined') {\n fieldCountPrevRow = 0;\n continue;\n }\n else if (fieldCount > 1) {\n delta += Math.abs(fieldCount - fieldCountPrevRow);\n fieldCountPrevRow = fieldCount;\n }\n }\n if (preview.data.length > 0)\n avgFieldCount /= preview.data.length - emptyLinesCount;\n if ((typeof bestDelta === 'undefined' || delta > bestDelta) && avgFieldCount > 1.99) {\n bestDelta = delta;\n bestDelim = delim;\n }\n }\n this._config.delimiter = bestDelim;\n return {\n successful: !!bestDelim,\n bestDelimiter: bestDelim\n };\n }\n addError(type, code, msg, row) {\n this._results.errors.push({\n type: type,\n code: code,\n message: msg,\n row: row\n });\n }\n}\nfunction guessLineEndings(input, quoteChar) {\n input = input.substr(0, 1024 * 1024); // max length 1 MB\n // Replace all the text inside quotes\n var re = new RegExp(escapeRegExp(quoteChar) + '([^]*?)' + escapeRegExp(quoteChar), 'gm');\n input = input.replace(re, '');\n var r = input.split('\\r');\n var n = input.split('\\n');\n var nAppearsFirst = n.length > 1 && n[0].length < r[0].length;\n if (r.length === 1 || nAppearsFirst)\n return '\\n';\n var numWithN = 0;\n for (var i = 0; i < r.length; i++) {\n if (r[i][0] === '\\n')\n numWithN++;\n }\n return numWithN >= r.length / 2 ? '\\r\\n' : '\\r';\n}\n/** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions */\nfunction escapeRegExp(string) {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'); // $& means the whole matched string\n}\n/** The core parser implements speedy and correct CSV parsing */\nfunction Parser(config) {\n // Unpack the config object\n config = config || {};\n var delim = config.delimiter;\n var newline = config.newline;\n var comments = config.comments;\n var step = config.step;\n var preview = config.preview;\n var fastMode = config.fastMode;\n var quoteChar;\n /** Allows for no quoteChar by setting quoteChar to undefined in config */\n if (config.quoteChar === undefined) {\n quoteChar = '\"';\n }\n else {\n quoteChar = config.quoteChar;\n }\n var escapeChar = quoteChar;\n if (config.escapeChar !== undefined) {\n escapeChar = config.escapeChar;\n }\n // Delimiter must be valid\n if (typeof delim !== 'string' || Papa.BAD_DELIMITERS.indexOf(delim) > -1)\n delim = ',';\n // Comment character must be valid\n if (comments === delim)\n throw new Error('Comment character same as delimiter');\n else if (comments === true)\n comments = '#';\n else if (typeof comments !== 'string' || Papa.BAD_DELIMITERS.indexOf(comments) > -1)\n comments = false;\n // Newline must be valid: \\r, \\n, or \\r\\n\n if (newline !== '\\n' && newline !== '\\r' && newline !== '\\r\\n')\n newline = '\\n';\n // We're gonna need these at the Parser scope\n var cursor = 0;\n var aborted = false;\n // @ts-expect-error\n this.parse = function (input, baseIndex, ignoreLastRow) {\n // For some reason, in Chrome, this speeds things up (!?)\n if (typeof input !== 'string')\n throw new Error('Input must be a string');\n // We don't need to compute some of these every time parse() is called,\n // but having them in a more local scope seems to perform better\n var inputLen = input.length, delimLen = delim.length, newlineLen = newline.length, commentsLen = comments.length;\n var stepIsFunction = isFunction(step);\n // Establish starting state\n cursor = 0;\n var data = [], errors = [], row = [], lastCursor = 0;\n if (!input)\n return returnable();\n if (fastMode || (fastMode !== false && input.indexOf(quoteChar) === -1)) {\n var rows = input.split(newline);\n for (var i = 0; i < rows.length; i++) {\n const row = rows[i];\n cursor += row.length;\n if (i !== rows.length - 1)\n cursor += newline.length;\n else if (ignoreLastRow)\n return returnable();\n if (comments && row.substr(0, commentsLen) === comments)\n continue;\n if (stepIsFunction) {\n data = [];\n pushRow(row.split(delim));\n doStep();\n if (aborted)\n return returnable();\n }\n else\n pushRow(row.split(delim));\n if (preview && i >= preview) {\n data = data.slice(0, preview);\n return returnable(true);\n }\n }\n return returnable();\n }\n var nextDelim = input.indexOf(delim, cursor);\n var nextNewline = input.indexOf(newline, cursor);\n var quoteCharRegex = new RegExp(escapeRegExp(escapeChar) + escapeRegExp(quoteChar), 'g');\n var quoteSearch;\n // Parser loop\n for (;;) {\n // Field has opening quote\n if (input[cursor] === quoteChar) {\n // Start our search for the closing quote where the cursor is\n quoteSearch = cursor;\n // Skip the opening quote\n cursor++;\n for (;;) {\n // Find closing quote\n quoteSearch = input.indexOf(quoteChar, quoteSearch + 1);\n //No other quotes are found - no other delimiters\n if (quoteSearch === -1) {\n if (!ignoreLastRow) {\n // No closing quote... what a pity\n errors.push({\n type: 'Quotes',\n code: 'MissingQuotes',\n message: 'Quoted field unterminated',\n row: data.length, // row has yet to be inserted\n index: cursor\n });\n }\n return finish();\n }\n // Closing quote at EOF\n if (quoteSearch === inputLen - 1) {\n var value = input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar);\n return finish(value);\n }\n // If this quote is escaped, it's part of the data; skip it\n // If the quote character is the escape character, then check if the next character is the escape character\n if (quoteChar === escapeChar && input[quoteSearch + 1] === escapeChar) {\n quoteSearch++;\n continue;\n }\n // If the quote character is not the escape character, then check if the previous character was the escape character\n if (quoteChar !== escapeChar &&\n quoteSearch !== 0 &&\n input[quoteSearch - 1] === escapeChar) {\n continue;\n }\n // Check up to nextDelim or nextNewline, whichever is closest\n var checkUpTo = nextNewline === -1 ? nextDelim : Math.min(nextDelim, nextNewline);\n var spacesBetweenQuoteAndDelimiter = extraSpaces(checkUpTo);\n // Closing quote followed by delimiter or 'unnecessary spaces + delimiter'\n if (input[quoteSearch + 1 + spacesBetweenQuoteAndDelimiter] === delim) {\n row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar));\n cursor = quoteSearch + 1 + spacesBetweenQuoteAndDelimiter + delimLen;\n nextDelim = input.indexOf(delim, cursor);\n nextNewline = input.indexOf(newline, cursor);\n if (stepIsFunction) {\n doStep();\n if (aborted)\n return returnable();\n }\n if (preview && data.length >= preview)\n return returnable(true);\n break;\n }\n var spacesBetweenQuoteAndNewLine = extraSpaces(nextNewline);\n // Closing quote followed by newline or 'unnecessary spaces + newLine'\n if (input.substr(quoteSearch + 1 + spacesBetweenQuoteAndNewLine, newlineLen) === newline) {\n row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar));\n saveRow(quoteSearch + 1 + spacesBetweenQuoteAndNewLine + newlineLen);\n nextDelim = input.indexOf(delim, cursor); // because we may have skipped the nextDelim in the quoted field\n if (stepIsFunction) {\n doStep();\n if (aborted)\n return returnable();\n }\n if (preview && data.length >= preview)\n return returnable(true);\n break;\n }\n // Checks for valid closing quotes are complete (escaped quotes or quote followed by EOF/delimiter/newline) -- assume these quotes are part of an invalid text string\n errors.push({\n type: 'Quotes',\n code: 'InvalidQuotes',\n message: 'Trailing quote on quoted field is malformed',\n row: data.length, // row has yet to be inserted\n index: cursor\n });\n quoteSearch++;\n continue;\n }\n if (stepIsFunction) {\n doStep();\n if (aborted)\n return returnable();\n }\n if (preview && data.length >= preview)\n return returnable(true);\n continue;\n }\n // Comment found at start of new line\n if (comments && row.length === 0 && input.substr(cursor, commentsLen) === comments) {\n if (nextNewline === -1)\n // Comment ends at EOF\n return returnable();\n cursor = nextNewline + newlineLen;\n nextNewline = input.indexOf(newline, cursor);\n nextDelim = input.indexOf(delim, cursor);\n continue;\n }\n // Next delimiter comes before next newline, so we've reached end of field\n if (nextDelim !== -1 && (nextDelim < nextNewline || nextNewline === -1)) {\n row.push(input.substring(cursor, nextDelim));\n cursor = nextDelim + delimLen;\n nextDelim = input.indexOf(delim, cursor);\n continue;\n }\n // End of row\n if (nextNewline !== -1) {\n row.push(input.substring(cursor, nextNewline));\n saveRow(nextNewline + newlineLen);\n if (stepIsFunction) {\n doStep();\n if (aborted)\n return returnable();\n }\n if (preview && data.length >= preview)\n return returnable(true);\n continue;\n }\n break;\n }\n return finish();\n function pushRow(row) {\n data.push(row);\n lastCursor = cursor;\n }\n /**\n * checks if there are extra spaces after closing quote and given index without any text\n * if Yes, returns the number of spaces\n */\n function extraSpaces(index) {\n var spaceLength = 0;\n if (index !== -1) {\n var textBetweenClosingQuoteAndIndex = input.substring(quoteSearch + 1, index);\n if (textBetweenClosingQuoteAndIndex && textBetweenClosingQuoteAndIndex.trim() === '') {\n spaceLength = textBetweenClosingQuoteAndIndex.length;\n }\n }\n return spaceLength;\n }\n /**\n * Appends the remaining input from cursor to the end into\n * row, saves the row, calls step, and returns the results.\n */\n function finish(value) {\n if (ignoreLastRow)\n return returnable();\n if (typeof value === 'undefined')\n value = input.substr(cursor);\n row.push(value);\n cursor = inputLen; // important in case parsing is paused\n pushRow(row);\n if (stepIsFunction)\n doStep();\n return returnable();\n }\n /**\n * Appends the current row to the results. It sets the cursor\n * to newCursor and finds the nextNewline. The caller should\n * take care to execute user's step function and check for\n * preview and end parsing if necessary.\n */\n function saveRow(newCursor) {\n cursor = newCursor;\n pushRow(row);\n row = [];\n nextNewline = input.indexOf(newline, cursor);\n }\n /** Returns an object with the results, errors, and meta. */\n function returnable(stopped, step) {\n var isStep = step || false;\n return {\n data: isStep ? data[0] : data,\n errors: errors,\n meta: {\n delimiter: delim,\n linebreak: newline,\n aborted: aborted,\n truncated: !!stopped,\n cursor: lastCursor + (baseIndex || 0)\n }\n };\n }\n /** Executes the user's step function and resets data & errors. */\n function doStep() {\n step(returnable(undefined, true));\n data = [];\n errors = [];\n }\n };\n /** Sets the abort flag */\n // @ts-expect-error\n this.abort = function () {\n aborted = true;\n };\n /** Gets the cursor position */\n // @ts-expect-error\n this.getCharIndex = function () {\n return cursor;\n };\n}\n/** Makes a deep copy of an array or object (mostly) */\nfunction copy(obj) {\n if (typeof obj !== 'object' || obj === null)\n return obj;\n var cpy = Array.isArray(obj) ? [] : {};\n for (var key in obj)\n cpy[key] = copy(obj[key]);\n return cpy;\n}\nfunction isFunction(func) {\n return typeof func === 'function';\n}\nconst Papa = {\n parse: CsvToJson,\n unparse: JsonToCsv,\n RECORD_SEP: String.fromCharCode(30),\n UNIT_SEP: String.fromCharCode(31),\n BYTE_ORDER_MARK,\n BAD_DELIMITERS: ['\\r', '\\n', '\"', BYTE_ORDER_MARK],\n WORKERS_SUPPORTED: false, // !IS_WORKER && !!globalThis.Worker\n NODE_STREAM_INPUT: 1,\n // Configurable chunk sizes for local and remote files, respectively\n LocalChunkSize: 1024 * 1024 * 10, // 10 M,\n RemoteChunkSize: 1024 * 1024 * 5, // 5 M,\n DefaultDelimiter: ',', // Used if not specified and detection fail,\n // Exposed for testing and development only\n Parser: Parser,\n ParserHandle: ParserHandle,\n // BEGIN FORK\n ChunkStreamer: ChunkStreamer\n};\nexport default Papa;\n", "// @ts-nocheck\n// A custom papaparse `Streamer` for async iterators\n// Ideally this can be contributed back to papaparse\n// Or papaparse can expose Streamer API so we can extend without forking.\n/* eslint-disable no-invalid-this */\n// Note: papaparse is not an ES6 module\nimport Papa from \"./papaparse.js\";\nconst { ChunkStreamer } = Papa;\nexport default class AsyncIteratorStreamer extends ChunkStreamer {\n textDecoder = new TextDecoder(this._config.encoding);\n constructor(config = {}) {\n super(config);\n }\n // Implement ChunkStreamer base class methods\n // this.pause = function() {\n // ChunkStreamer.prototype.pause.apply(this, arguments);\n // };\n // this.resume = function() {\n // Chu