@vielzeug/tabula 26.11.13 → 26.11.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/csv.cjs +6 -6
- package/dist/csv.cjs.map +1 -1
- package/dist/csv.d.ts +30 -12
- package/dist/csv.d.ts.map +1 -1
- package/dist/csv.js +34 -24
- package/dist/csv.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/stream.cjs +1 -1
- package/dist/stream.cjs.map +1 -1
- package/dist/stream.d.ts +3 -3
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +5 -9
- package/dist/stream.js.map +1 -1
- package/dist/tabula.cjs +6 -6
- package/dist/tabula.cjs.map +1 -1
- package/dist/tabula.iife.js +6 -6
- package/dist/tabula.iife.js.map +1 -1
- package/dist/tabula.js +40 -34
- package/dist/tabula.js.map +1 -1
- package/dist/typed.cjs +1 -1
- package/dist/typed.cjs.map +1 -1
- package/dist/typed.js +10 -10
- package/dist/typed.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/csv.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const e=require("./errors.cjs");var t=`\r
|
|
2
|
-
`;function detectDelimiter(e){let t=e.slice(0,2048).split(/[\n\r]/,1)[0]??``,n=`,`,r=-1;for(let e of[` `,`;`,`,`]){let i=0,a=!1;for(let n=0;n<t.length;n+=1){let r=t[n];r===`"`?a=!a:r===e&&!a&&(i+=1)}i>r&&(n=e,r=i)}return r>0?n:`,`}var CsvScanner=class{delimiter;skipEmptyLines;field=``;row=[];inQuotes=!1;fieldWasQuoted=!1;pendingCr=!1;started=!1;line=1;rowLine=1;constructor(e,t
|
|
3
|
-
`)||(this.started||
|
|
4
|
-
`),this.inQuotes?(
|
|
5
|
-
`&&(this.line+=1),
|
|
6
|
-
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=
|
|
7
|
-
`)||e.includes(`\r`)||e!==e.trim()}function serializeCell(e,t,n){let r=e==null?``:String(e);return!n&&!needsQuotes(r,t)?r:`"${r.replaceAll(`"`,`""`)}"`}function toCsv(e,n={}){let r=n.delimiter
|
|
2
|
+
`;function assertSingleChar(t){if(t.length!==1)throw new e.TabulaParseError(`The delimiter must be a single character, got "${t}".`);return t}function detectDelimiter(e){let t=e.slice(0,2048).split(/[\n\r]/,1)[0]??``,n=`,`,r=-1;for(let e of[` `,`;`,`,`]){let i=0,a=!1;for(let n=0;n<t.length;n+=1){let r=t[n];r===`"`?a=!a:r===e&&!a&&(i+=1)}i>r&&(n=e,r=i)}return r>0?n:`,`}var CsvScanner=class{delimiter;skipEmptyLines;buffer=``;field=``;row=[];inQuotes=!1;fieldWasQuoted=!1;pendingCr=!1;started=!1;line=1;rowLine=1;constructor(e={}){this.delimiter=e.delimiter===void 0?void 0:assertSingleChar(e.delimiter),this.skipEmptyLines=e.skipEmptyLines!==!1}push(e,t){if(this.delimiter===void 0){if(this.buffer+=e,!/[\n\r]/.test(this.buffer))return;this.delimiter=detectDelimiter(this.buffer),e=this.buffer,this.buffer=``}this.scan(e,t)}finish(e){if(this.buffer!==``){let t=this.buffer;this.buffer=``,this.delimiter=this.delimiter??detectDelimiter(t),this.scan(t,e)}(this.field!==``||this.row.length>0||this.fieldWasQuoted)&&this.endRow(e),this.pendingCr=!1}scan(e,t){let n=this.delimiter;for(let r=0;r<e.length;r+=1){let i=e[r];this.pendingCr&&(this.pendingCr=!1,i===`
|
|
3
|
+
`)||(this.started||r!==0||i!==``)&&(this.started=!0,i===`\r`&&(this.pendingCr=!0,i=`
|
|
4
|
+
`),this.inQuotes?(i===`
|
|
5
|
+
`&&(this.line+=1),i===`"`?e[r+1]===`"`?(this.field+=`"`,r+=1):this.inQuotes=!1:this.field+=i):i===`"`&&!this.fieldWasQuoted&&this.field===``?(this.inQuotes=!0,this.fieldWasQuoted=!0):i===`"`?this.field+=`"`:i===n?(this.row.push(this.field),this.field=``,this.fieldWasQuoted=!1):i===`
|
|
6
|
+
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=i)}}endRow(e){this.row.push(this.field),this.field=``,this.row.length===1&&this.row[0]===``&&this.skipEmptyLines||e(this.row,this.rowLine),this.row=[],this.line+=1,this.rowLine=this.line}};function parseGrid(e,t={}){let n=new CsvScanner(t),r=[];return n.push(e,e=>r.push(e)),n.finish(e=>r.push(e)),r}function parseTrackedRecords(t,n={}){if(n.headers!==void 0&&n.headers.length===0)throw new e.TabulaParseError(`headers must name at least one column`);let r=new CsvScanner(n),i=[];if(r.push(t,(e,t)=>i.push([e,t])),r.finish((e,t)=>i.push([e,t])),i.length===0)return[];let a=n.headers??i[0][0];return(n.headers===void 0?i.slice(1):i).map(([e,t])=>{let n={};return a.forEach((t,r)=>{let i=e[r];i!==void 0&&(n[t]=i)}),{line:t,record:n}})}function parseRecords(e,t={}){return parseTrackedRecords(e,t).map(({record:e})=>e)}function needsQuotes(e,t){return e.includes(t)||e.includes(`"`)||e.includes(`
|
|
7
|
+
`)||e.includes(`\r`)||e!==e.trim()}function serializeCell(e,t,n){let r=e==null?``:String(e);return!n&&!needsQuotes(r,t)?r:`"${r.replaceAll(`"`,`""`)}"`}function toCsv(e,n={}){let r=assertSingleChar(n.delimiter??`,`),i=n.newline??t,a=n.quoteAll===!0,o=[];for(let t of e)o.push(t.map(e=>serializeCell(e,r,a)).join(r));return o.join(i)+(o.length>0?i:``)}function recordsToCsv(e,t={}){let n=[...e],r=t.columns??[...new Set(n.flatMap(e=>Object.keys(e)))],i=[r];for(let e of n)i.push(r.map(t=>e[t]??``));return toCsv(i,t)}exports.CsvScanner=CsvScanner,exports.parseGrid=parseGrid,exports.parseRecords=parseRecords,exports.parseTrackedRecords=parseTrackedRecords,exports.recordsToCsv=recordsToCsv,exports.toCsv=toCsv;
|
|
8
8
|
//# sourceMappingURL=csv.cjs.map
|
package/dist/csv.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.cjs","names":["TabulaParseError","require_errors","DEFAULT_NEWLINE","firstLine","text","best","bestCount","candidate","count","quoted","i","char","delimiter","skipEmptyLines","onRow","scanner","options","rows","headers","record","cell","cells","index","header","row","value","raw","quoteAll","newline","lines","list","records","columns","column"],"sources":["../src/csv.ts"],"sourcesContent":["import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nexport function detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows. Rows may span\n * chunks; `finish()` flushes the pending row. Handles BOM, LF/CRLF/CR, quoted\n * fields with embedded delimiters, newlines and `\"\"` escapes.\n */\nexport class CsvScanner {\n private readonly delimiter: string;\n private readonly skipEmptyLines: boolean;\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(delimiter: string, skipEmptyLines = true) {\n this.delimiter = delimiter;\n this.skipEmptyLines = skipEmptyLines;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === this.delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nfunction scan(text: string, options: TabulaParseOptions, onRow: (row: string[], line: number) => void): void {\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n scanner.push(text, onRow);\n scanner.finish(onRow);\n}\n\n/** A row paired with the 1-based physical line it started on. */\nexport type LineRow = { line: number; record: Record<string, string> };\n\n/** Parse into records while keeping each row's physical start line (for error reporting). */\nexport function parseRecordsWithLines(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const rows: { cells: string[]; line: number }[] = [];\n scan(text, options, (cells, line) => rows.push({ cells, line }));\n if (rows.length === 0) return [];\n const headers = options.headers ?? ((rows[0] as { cells: string[] }).cells as readonly string[]);\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(({ cells, line }) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const rows: string[][] = [];\n scan(text, options, (row) => rows.push(row));\n return rows;\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseRecordsWithLines(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = options.delimiter ?? ',';\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/** Serialize records to CSV with a header row; `columns` overrides column order. */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"csv.cjs","names":["TabulaParseError","require_errors","DEFAULT_NEWLINE","delimiter","firstLine","text","best","bestCount","candidate","count","quoted","i","char","options","onRow","scanner","rows","row","cells","line","headers","record","cell","index","header","value","raw","quoteAll","newline","lines","list","records","columns","column"],"sources":["../src/csv.ts"],"sourcesContent":["import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** A delimiter the scanner can match: exactly one character. */\nfunction assertSingleChar(delimiter: string): string {\n if (delimiter.length !== 1) {\n throw new TabulaParseError(`The delimiter must be a single character, got \"${delimiter}\".`);\n }\n return delimiter;\n}\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nfunction detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows with the\n * 1-based physical line each started on. Rows may span chunks; `finish()`\n * flushes the pending row. Handles BOM, LF/CRLF/CR, quoted fields with\n * embedded delimiters, newlines and `\"\"` escapes.\n *\n * Without a pinned `delimiter` the scanner accumulates until the first line\n * completes, auto-detects, and starts emitting rows.\n */\nexport class CsvScanner {\n private delimiter: string | undefined;\n private readonly skipEmptyLines: boolean;\n private buffer = '';\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(options: TabulaParseOptions = {}) {\n this.delimiter = options.delimiter === undefined ? undefined : assertSingleChar(options.delimiter);\n this.skipEmptyLines = options.skipEmptyLines !== false;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n if (this.delimiter === undefined) {\n this.buffer += text;\n // Auto-detection needs a complete first line; hold the chunks until then.\n if (!/[\\n\\r]/.test(this.buffer)) return;\n this.delimiter = detectDelimiter(this.buffer);\n text = this.buffer;\n this.buffer = '';\n }\n this.scan(text, onRow);\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.buffer !== '') {\n const text = this.buffer;\n this.buffer = '';\n this.delimiter = this.delimiter ?? detectDelimiter(text);\n this.scan(text, onRow);\n }\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private scan(text: string, onRow: (row: string[], line: number) => void): void {\n const delimiter = this.delimiter as string;\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const scanner = new CsvScanner(options);\n const rows: string[][] = [];\n scanner.push(text, (row) => rows.push(row));\n scanner.finish((row) => rows.push(row));\n return rows;\n}\n\n/** A row paired with the 1-based physical line it started on. */\ntype LineRow = { line: number; record: Record<string, string> };\n\n/**\n * Parse into records while keeping each row's physical start line: the shared\n * core of {@link parseRecords} and the typed reader. Headers default to the\n * first row; explicit `headers` make it headerless. Extra cells are dropped,\n * short rows leave keys off.\n */\nexport function parseTrackedRecords(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const scanner = new CsvScanner(options);\n const rows: [cells: string[], line: number][] = [];\n scanner.push(text, (cells, line) => rows.push([cells, line]));\n scanner.finish((cells, line) => rows.push([cells, line]));\n if (rows.length === 0) return [];\n const headers = options.headers ?? rows[0][0];\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(([cells, line]) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseTrackedRecords(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = assertSingleChar(options.delimiter ?? ',');\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/**\n * Serialize records to CSV with a header row. Columns are the union of every\n * record's keys in first-seen order unless `columns` overrides the order.\n */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n"],"mappings":"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"}
|
package/dist/csv.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import type { TabulaParseOptions, TabulaRecordOptions, TabulaRecordSerializeOptions, TabulaSerializeOptions } from './types';
|
|
2
|
-
/** Detect the delimiter from the first line among comma, semicolon, and tab. */
|
|
3
|
-
export declare function detectDelimiter(text: string): string;
|
|
4
2
|
/**
|
|
5
|
-
* Push-based RFC 4180 scanner: feed text, receive complete rows
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* Push-based RFC 4180 scanner: feed text, receive complete rows with the
|
|
4
|
+
* 1-based physical line each started on. Rows may span chunks; `finish()`
|
|
5
|
+
* flushes the pending row. Handles BOM, LF/CRLF/CR, quoted fields with
|
|
6
|
+
* embedded delimiters, newlines and `""` escapes.
|
|
7
|
+
*
|
|
8
|
+
* Without a pinned `delimiter` the scanner accumulates until the first line
|
|
9
|
+
* completes, auto-detects, and starts emitting rows.
|
|
8
10
|
*/
|
|
9
11
|
export declare class CsvScanner {
|
|
10
|
-
private
|
|
12
|
+
private delimiter;
|
|
11
13
|
private readonly skipEmptyLines;
|
|
14
|
+
private buffer;
|
|
12
15
|
private field;
|
|
13
16
|
private row;
|
|
14
17
|
private inQuotes;
|
|
@@ -17,20 +20,31 @@ export declare class CsvScanner {
|
|
|
17
20
|
private started;
|
|
18
21
|
private line;
|
|
19
22
|
private rowLine;
|
|
20
|
-
constructor(
|
|
23
|
+
constructor(options?: TabulaParseOptions);
|
|
21
24
|
/** Emit every completed row with the 1-based physical line it started on. */
|
|
22
25
|
push(text: string, onRow: (row: string[], line: number) => void): void;
|
|
23
26
|
finish(onRow: (row: string[], line: number) => void): void;
|
|
27
|
+
private scan;
|
|
24
28
|
private endRow;
|
|
25
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain
|
|
32
|
+
* the delimiter, newlines, and escaped quotes; blank lines are skipped by
|
|
33
|
+
* default. This is the untyped core every other tabula reader builds on.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseGrid(text: string, options?: TabulaParseOptions): string[][];
|
|
26
36
|
/** A row paired with the 1-based physical line it started on. */
|
|
27
|
-
|
|
37
|
+
type LineRow = {
|
|
28
38
|
line: number;
|
|
29
39
|
record: Record<string, string>;
|
|
30
40
|
};
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Parse into records while keeping each row's physical start line: the shared
|
|
43
|
+
* core of {@link parseRecords} and the typed reader. Headers default to the
|
|
44
|
+
* first row; explicit `headers` make it headerless. Extra cells are dropped,
|
|
45
|
+
* short rows leave keys off.
|
|
46
|
+
*/
|
|
47
|
+
export declare function parseTrackedRecords(text: string, options?: TabulaRecordOptions): LineRow[];
|
|
34
48
|
/**
|
|
35
49
|
* Parse CSV text into `Record<string, string>` rows. By default the first row
|
|
36
50
|
* supplies the headers; pass explicit `headers` to read a headerless document.
|
|
@@ -39,6 +53,10 @@ export declare function parseGrid(text: string, options?: TabulaParseOptions): s
|
|
|
39
53
|
export declare function parseRecords(text: string, options?: TabulaRecordOptions): Record<string, string>[];
|
|
40
54
|
/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */
|
|
41
55
|
export declare function toCsv(rows: Iterable<readonly unknown[]>, options?: TabulaSerializeOptions): string;
|
|
42
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Serialize records to CSV with a header row. Columns are the union of every
|
|
58
|
+
* record's keys in first-seen order unless `columns` overrides the order.
|
|
59
|
+
*/
|
|
43
60
|
export declare function recordsToCsv(records: Iterable<Record<string, unknown>>, options?: TabulaRecordSerializeOptions): string;
|
|
61
|
+
export {};
|
|
44
62
|
//# sourceMappingURL=csv.d.ts.map
|
package/dist/csv.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,4BAA4B,EAC5B,sBAAsB,EACvB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"csv.d.ts","sourceRoot":"","sources":["../src/csv.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,4BAA4B,EAC5B,sBAAsB,EACvB,MAAM,SAAS,CAAC;AAiCjB;;;;;;;;GAQG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,SAAS,CAAqB;IACtC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,KAAK,CAAM;IACnB,OAAO,CAAC,GAAG,CAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,IAAI,CAAK;IACjB,OAAO,CAAC,OAAO,CAAK;gBAER,OAAO,GAAE,kBAAuB;IAK5C,6EAA6E;IAC7E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAYtE,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAW1D,OAAO,CAAC,IAAI;IAqDZ,OAAO,CAAC,MAAM;CASf;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,EAAE,EAAE,CAMpF;AAED,iEAAiE;AACjE,KAAK,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAEhE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,EAAE,CAmB9F;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAEtG;AAkBD,wFAAwF;AACxF,wBAAgB,KAAK,CAAC,IAAI,EAAE,QAAQ,CAAC,SAAS,OAAO,EAAE,CAAC,EAAE,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAStG;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC1C,OAAO,GAAE,4BAAiC,GACzC,MAAM,CAMR"}
|
package/dist/csv.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { TabulaParseError as e } from "./errors.js";
|
|
2
2
|
// src/csv.ts
|
|
3
3
|
var t = "\r\n";
|
|
4
|
+
function assertSingleChar(t) {
|
|
5
|
+
if (t.length !== 1) throw new e(`The delimiter must be a single character, got "${t}".`);
|
|
6
|
+
return t;
|
|
7
|
+
}
|
|
4
8
|
function detectDelimiter(e) {
|
|
5
9
|
let t = e.slice(0, 2048).split(/[\n\r]/, 1)[0] ?? "", n = ",", r = -1;
|
|
6
10
|
for (let e of [
|
|
@@ -20,6 +24,7 @@ function detectDelimiter(e) {
|
|
|
20
24
|
var CsvScanner = class {
|
|
21
25
|
delimiter;
|
|
22
26
|
skipEmptyLines;
|
|
27
|
+
buffer = "";
|
|
23
28
|
field = "";
|
|
24
29
|
row = [];
|
|
25
30
|
inQuotes = !1;
|
|
@@ -28,37 +33,46 @@ var CsvScanner = class {
|
|
|
28
33
|
started = !1;
|
|
29
34
|
line = 1;
|
|
30
35
|
rowLine = 1;
|
|
31
|
-
constructor(e
|
|
32
|
-
this.delimiter = e, this.skipEmptyLines =
|
|
36
|
+
constructor(e = {}) {
|
|
37
|
+
this.delimiter = e.delimiter === void 0 ? void 0 : assertSingleChar(e.delimiter), this.skipEmptyLines = e.skipEmptyLines !== !1;
|
|
33
38
|
}
|
|
34
39
|
push(e, t) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
this.
|
|
40
|
+
if (this.delimiter === void 0) {
|
|
41
|
+
if (this.buffer += e, !/[\n\r]/.test(this.buffer)) return;
|
|
42
|
+
this.delimiter = detectDelimiter(this.buffer), e = this.buffer, this.buffer = "";
|
|
38
43
|
}
|
|
44
|
+
this.scan(e, t);
|
|
39
45
|
}
|
|
40
46
|
finish(e) {
|
|
47
|
+
if (this.buffer !== "") {
|
|
48
|
+
let t = this.buffer;
|
|
49
|
+
this.buffer = "", this.delimiter = this.delimiter ?? detectDelimiter(t), this.scan(t, e);
|
|
50
|
+
}
|
|
41
51
|
(this.field !== "" || this.row.length > 0 || this.fieldWasQuoted) && this.endRow(e), this.pendingCr = !1;
|
|
42
52
|
}
|
|
53
|
+
scan(e, t) {
|
|
54
|
+
let n = this.delimiter;
|
|
55
|
+
for (let r = 0; r < e.length; r += 1) {
|
|
56
|
+
let i = e[r];
|
|
57
|
+
this.pendingCr && (this.pendingCr = !1, i === "\n") || (this.started || r !== 0 || i !== "") && (this.started = !0, i === "\r" && (this.pendingCr = !0, i = "\n"), this.inQuotes ? (i === "\n" && (this.line += 1), i === "\"" ? e[r + 1] === "\"" ? (this.field += "\"", r += 1) : this.inQuotes = !1 : this.field += i) : i === "\"" && !this.fieldWasQuoted && this.field === "" ? (this.inQuotes = !0, this.fieldWasQuoted = !0) : i === "\"" ? this.field += "\"" : i === n ? (this.row.push(this.field), this.field = "", this.fieldWasQuoted = !1) : i === "\n" ? (this.endRow(t), this.fieldWasQuoted = !1) : this.field += i);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
43
60
|
endRow(e) {
|
|
44
61
|
this.row.push(this.field), this.field = "", this.row.length === 1 && this.row[0] === "" && this.skipEmptyLines || e(this.row, this.rowLine), this.row = [], this.line += 1, this.rowLine = this.line;
|
|
45
62
|
}
|
|
46
63
|
};
|
|
47
|
-
function
|
|
48
|
-
let
|
|
49
|
-
r.push(e, n)
|
|
64
|
+
function parseGrid(e, t = {}) {
|
|
65
|
+
let n = new CsvScanner(t), r = [];
|
|
66
|
+
return n.push(e, (e) => r.push(e)), n.finish((e) => r.push(e)), r;
|
|
50
67
|
}
|
|
51
|
-
function
|
|
68
|
+
function parseTrackedRecords(t, n = {}) {
|
|
52
69
|
if (n.headers !== void 0 && n.headers.length === 0) throw new e("headers must name at least one column");
|
|
53
|
-
let r = [];
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
})), r.length === 0) return [];
|
|
58
|
-
let i = n.headers ?? r[0].cells;
|
|
59
|
-
return (n.headers === void 0 ? r.slice(1) : r).map(({ cells: e, line: t }) => {
|
|
70
|
+
let r = new CsvScanner(n), i = [];
|
|
71
|
+
if (r.push(t, (e, t) => i.push([e, t])), r.finish((e, t) => i.push([e, t])), i.length === 0) return [];
|
|
72
|
+
let a = n.headers ?? i[0][0];
|
|
73
|
+
return (n.headers === void 0 ? i.slice(1) : i).map(([e, t]) => {
|
|
60
74
|
let n = {};
|
|
61
|
-
return
|
|
75
|
+
return a.forEach((t, r) => {
|
|
62
76
|
let i = e[r];
|
|
63
77
|
i !== void 0 && (n[t] = i);
|
|
64
78
|
}), {
|
|
@@ -67,12 +81,8 @@ function parseRecordsWithLines(t, n = {}) {
|
|
|
67
81
|
};
|
|
68
82
|
});
|
|
69
83
|
}
|
|
70
|
-
function parseGrid(e, t = {}) {
|
|
71
|
-
let n = [];
|
|
72
|
-
return scan(e, t, (e) => n.push(e)), n;
|
|
73
|
-
}
|
|
74
84
|
function parseRecords(e, t = {}) {
|
|
75
|
-
return
|
|
85
|
+
return parseTrackedRecords(e, t).map(({ record: e }) => e);
|
|
76
86
|
}
|
|
77
87
|
function needsQuotes(e, t) {
|
|
78
88
|
return e.includes(t) || e.includes("\"") || e.includes("\n") || e.includes("\r") || e !== e.trim();
|
|
@@ -82,7 +92,7 @@ function serializeCell(e, t, n) {
|
|
|
82
92
|
return !n && !needsQuotes(r, t) ? r : `"${r.replaceAll("\"", "\"\"")}"`;
|
|
83
93
|
}
|
|
84
94
|
function toCsv(e, n = {}) {
|
|
85
|
-
let r = n.delimiter ?? ",", i = n.newline ?? t, a = n.quoteAll === !0, o = [];
|
|
95
|
+
let r = assertSingleChar(n.delimiter ?? ","), i = n.newline ?? t, a = n.quoteAll === !0, o = [];
|
|
86
96
|
for (let t of e) o.push(t.map((e) => serializeCell(e, r, a)).join(r));
|
|
87
97
|
return o.join(i) + (o.length > 0 ? i : "");
|
|
88
98
|
}
|
|
@@ -91,6 +101,6 @@ function recordsToCsv(e, t = {}) {
|
|
|
91
101
|
for (let e of n) i.push(r.map((t) => e[t] ?? ""));
|
|
92
102
|
return toCsv(i, t);
|
|
93
103
|
}
|
|
94
|
-
export { CsvScanner,
|
|
104
|
+
export { CsvScanner, parseGrid, parseRecords, parseTrackedRecords, recordsToCsv, toCsv };
|
|
95
105
|
|
|
96
106
|
//# sourceMappingURL=csv.js.map
|
package/dist/csv.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"csv.js","names":["DEFAULT_NEWLINE","firstLine","text","best","bestCount","candidate","count","quoted","i","char","delimiter","skipEmptyLines","onRow","scanner","options","TabulaParseError","rows","headers","record","cell","cells","index","header","row","value","raw","quoteAll","newline","lines","list","records","columns","column"],"sources":["../src/csv.ts"],"sourcesContent":["import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nexport function detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows. Rows may span\n * chunks; `finish()` flushes the pending row. Handles BOM, LF/CRLF/CR, quoted\n * fields with embedded delimiters, newlines and `\"\"` escapes.\n */\nexport class CsvScanner {\n private readonly delimiter: string;\n private readonly skipEmptyLines: boolean;\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(delimiter: string, skipEmptyLines = true) {\n this.delimiter = delimiter;\n this.skipEmptyLines = skipEmptyLines;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === this.delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nfunction scan(text: string, options: TabulaParseOptions, onRow: (row: string[], line: number) => void): void {\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n scanner.push(text, onRow);\n scanner.finish(onRow);\n}\n\n/** A row paired with the 1-based physical line it started on. */\nexport type LineRow = { line: number; record: Record<string, string> };\n\n/** Parse into records while keeping each row's physical start line (for error reporting). */\nexport function parseRecordsWithLines(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const rows: { cells: string[]; line: number }[] = [];\n scan(text, options, (cells, line) => rows.push({ cells, line }));\n if (rows.length === 0) return [];\n const headers = options.headers ?? ((rows[0] as { cells: string[] }).cells as readonly string[]);\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(({ cells, line }) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const rows: string[][] = [];\n scan(text, options, (row) => rows.push(row));\n return rows;\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseRecordsWithLines(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = options.delimiter ?? ',';\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/** Serialize records to CSV with a header row; `columns` overrides column order. */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"csv.js","names":["DEFAULT_NEWLINE","delimiter","TabulaParseError","firstLine","text","best","bestCount","candidate","count","quoted","i","char","options","onRow","scanner","rows","row","cells","line","headers","record","cell","index","header","value","raw","quoteAll","newline","lines","list","records","columns","column"],"sources":["../src/csv.ts"],"sourcesContent":["import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** A delimiter the scanner can match: exactly one character. */\nfunction assertSingleChar(delimiter: string): string {\n if (delimiter.length !== 1) {\n throw new TabulaParseError(`The delimiter must be a single character, got \"${delimiter}\".`);\n }\n return delimiter;\n}\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nfunction detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows with the\n * 1-based physical line each started on. Rows may span chunks; `finish()`\n * flushes the pending row. Handles BOM, LF/CRLF/CR, quoted fields with\n * embedded delimiters, newlines and `\"\"` escapes.\n *\n * Without a pinned `delimiter` the scanner accumulates until the first line\n * completes, auto-detects, and starts emitting rows.\n */\nexport class CsvScanner {\n private delimiter: string | undefined;\n private readonly skipEmptyLines: boolean;\n private buffer = '';\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(options: TabulaParseOptions = {}) {\n this.delimiter = options.delimiter === undefined ? undefined : assertSingleChar(options.delimiter);\n this.skipEmptyLines = options.skipEmptyLines !== false;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n if (this.delimiter === undefined) {\n this.buffer += text;\n // Auto-detection needs a complete first line; hold the chunks until then.\n if (!/[\\n\\r]/.test(this.buffer)) return;\n this.delimiter = detectDelimiter(this.buffer);\n text = this.buffer;\n this.buffer = '';\n }\n this.scan(text, onRow);\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.buffer !== '') {\n const text = this.buffer;\n this.buffer = '';\n this.delimiter = this.delimiter ?? detectDelimiter(text);\n this.scan(text, onRow);\n }\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private scan(text: string, onRow: (row: string[], line: number) => void): void {\n const delimiter = this.delimiter as string;\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const scanner = new CsvScanner(options);\n const rows: string[][] = [];\n scanner.push(text, (row) => rows.push(row));\n scanner.finish((row) => rows.push(row));\n return rows;\n}\n\n/** A row paired with the 1-based physical line it started on. */\ntype LineRow = { line: number; record: Record<string, string> };\n\n/**\n * Parse into records while keeping each row's physical start line: the shared\n * core of {@link parseRecords} and the typed reader. Headers default to the\n * first row; explicit `headers` make it headerless. Extra cells are dropped,\n * short rows leave keys off.\n */\nexport function parseTrackedRecords(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const scanner = new CsvScanner(options);\n const rows: [cells: string[], line: number][] = [];\n scanner.push(text, (cells, line) => rows.push([cells, line]));\n scanner.finish((cells, line) => rows.push([cells, line]));\n if (rows.length === 0) return [];\n const headers = options.headers ?? rows[0][0];\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(([cells, line]) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseTrackedRecords(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = assertSingleChar(options.delimiter ?? ',');\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/**\n * Serialize records to CSV with a header row. Columns are the union of every\n * record's keys in first-seen order unless `columns` overrides the order.\n */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./csv.cjs"),t=require("./errors.cjs"),n=require("./stream.cjs"),r=require("./typed.cjs");exports.CsvScanner=e.CsvScanner,exports.TabulaError=t.TabulaError,exports.TabulaParseError=t.TabulaParseError,exports.TabulaRowError=t.TabulaRowError,exports.TabulaValidationError=t.TabulaValidationError,exports.createCsvGridStream=n.createCsvGridStream,exports.
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./csv.cjs"),t=require("./errors.cjs"),n=require("./stream.cjs"),r=require("./typed.cjs");exports.CsvScanner=e.CsvScanner,exports.TabulaError=t.TabulaError,exports.TabulaParseError=t.TabulaParseError,exports.TabulaRowError=t.TabulaRowError,exports.TabulaValidationError=t.TabulaValidationError,exports.createCsvGridStream=n.createCsvGridStream,exports.parseGrid=e.parseGrid,exports.parseRecords=e.parseRecords,exports.parseTypedRecords=r.parseTypedRecords,exports.recordsToCsv=e.recordsToCsv,exports.toCsv=e.toCsv;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CsvScanner,
|
|
1
|
+
export { CsvScanner, parseGrid, parseRecords, recordsToCsv, toCsv } from './csv';
|
|
2
2
|
export { TabulaError, TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';
|
|
3
3
|
export { createCsvGridStream } from './stream';
|
|
4
4
|
export { parseTypedRecords } from './typed';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AACjF,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAChG,OAAO,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAC5C,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,mBAAmB,EACnB,4BAA4B,EAC5B,sBAAsB,GACvB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CsvScanner as e,
|
|
2
|
-
import { TabulaError as
|
|
3
|
-
import { createCsvGridStream as
|
|
4
|
-
import { parseTypedRecords as
|
|
5
|
-
export { e as CsvScanner,
|
|
1
|
+
import { CsvScanner as e, parseGrid as t, parseRecords as n, recordsToCsv as r, toCsv as i } from "./csv.js";
|
|
2
|
+
import { TabulaError as a, TabulaParseError as o, TabulaRowError as s, TabulaValidationError as c } from "./errors.js";
|
|
3
|
+
import { createCsvGridStream as l } from "./stream.js";
|
|
4
|
+
import { parseTypedRecords as u } from "./typed.js";
|
|
5
|
+
export { e as CsvScanner, a as TabulaError, o as TabulaParseError, s as TabulaRowError, c as TabulaValidationError, l as createCsvGridStream, t as parseGrid, n as parseRecords, u as parseTypedRecords, r as recordsToCsv, i as toCsv };
|
package/dist/stream.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./csv.cjs");function createCsvGridStream(t={}){let n=
|
|
1
|
+
const e=require("./csv.cjs");function createCsvGridStream(t={}){let n=new e.CsvScanner(t);return new TransformStream({flush(e){n.finish(t=>e.enqueue(t))},transform(e,t){n.push(e,e=>t.enqueue(e))}})}exports.createCsvGridStream=createCsvGridStream;
|
|
2
2
|
//# sourceMappingURL=stream.cjs.map
|
package/dist/stream.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.cjs","names":["CsvScanner","
|
|
1
|
+
{"version":3,"file":"stream.cjs","names":["CsvScanner","require_csv","scanner","options","controller","row","chunk"],"sources":["../src/stream.ts"],"sourcesContent":["import { CsvScanner } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out. The {@link CsvScanner} assembles rows across chunk\n * boundaries and auto-detects the delimiter once the first line is available\n * (unless pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n const scanner = new CsvScanner(options);\n return new TransformStream<string, string[]>({\n flush(controller) {\n scanner.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n"],"mappings":"A,6BAUA,SAAgB,oBAAoB,EAA8B,CAAC,EAAsC,CACvG,IAAME,EAAU,IAAIF,EAAAA,WAAWG,CAAO,EACtC,OAAO,IAAI,gBAAkC,CAC3C,MAAM,EAAY,CAChBD,EAAQ,OAAQ,GAAQE,EAAW,QAAQC,CAAG,CAAC,CACjD,EACA,UAAU,EAAO,EAAY,CAC3BH,EAAQ,KAAKI,EAAQ,GAAQF,EAAW,QAAQC,CAAG,CAAC,CACtD,CACF,CAAC,CACH,C"}
|
package/dist/stream.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { TabulaParseOptions } from './types';
|
|
2
2
|
/**
|
|
3
3
|
* A `TransformStream<string, string[]>`: push raw text chunks in, complete
|
|
4
|
-
* grid rows come out
|
|
5
|
-
* and the delimiter
|
|
6
|
-
* pinned via `delimiter`). Pair with `TextDecoderStream` for files and
|
|
4
|
+
* grid rows come out. The {@link CsvScanner} assembles rows across chunk
|
|
5
|
+
* boundaries and auto-detects the delimiter once the first line is available
|
|
6
|
+
* (unless pinned via `delimiter`). Pair with `TextDecoderStream` for files and
|
|
7
7
|
* `@vielzeug/familiar` workers for off-main-thread parsing of large exports.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createCsvGridStream(options?: TabulaParseOptions): TransformStream<string, string[]>;
|
package/dist/stream.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,kBAAuB,GAAG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAUvG"}
|
package/dist/stream.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import { CsvScanner as e
|
|
1
|
+
import { CsvScanner as e } from "./csv.js";
|
|
2
2
|
// src/stream.ts
|
|
3
|
-
function createCsvGridStream(
|
|
4
|
-
let
|
|
3
|
+
function createCsvGridStream(t = {}) {
|
|
4
|
+
let n = new e(t);
|
|
5
5
|
return new TransformStream({
|
|
6
6
|
flush(e) {
|
|
7
|
-
|
|
8
|
-
a !== "" && t.push(a, (t) => e.enqueue(t)), t.finish((t) => e.enqueue(t));
|
|
7
|
+
n.finish((t) => e.enqueue(t));
|
|
9
8
|
},
|
|
10
9
|
transform(e, t) {
|
|
11
|
-
|
|
12
|
-
if (a += e, !/[\n\r]/.test(a)) return;
|
|
13
|
-
ensureScanner().push(a, (e) => t.enqueue(e)), a = "";
|
|
14
|
-
} else i.push(e, (e) => t.enqueue(e));
|
|
10
|
+
n.push(e, (e) => t.enqueue(e));
|
|
15
11
|
}
|
|
16
12
|
});
|
|
17
13
|
}
|
package/dist/stream.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.js","names":["
|
|
1
|
+
{"version":3,"file":"stream.js","names":["scanner","CsvScanner","options","controller","row","chunk"],"sources":["../src/stream.ts"],"sourcesContent":["import { CsvScanner } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out. The {@link CsvScanner} assembles rows across chunk\n * boundaries and auto-detects the delimiter once the first line is available\n * (unless pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n const scanner = new CsvScanner(options);\n return new TransformStream<string, string[]>({\n flush(controller) {\n scanner.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n"],"mappings":";;AAUA,SAAgB,oBAAoB,IAA8B,CAAC,GAAsC;CACvG,IAAMA,IAAU,IAAIC,EAAWC,CAAO;CACtC,OAAO,IAAI,gBAAkC;EAC3C,MAAM,GAAY;GAChB,EAAQ,QAAQ,MAAQC,EAAW,QAAQC,CAAG,CAAC;EACjD;EACA,UAAU,GAAO,GAAY;GAC3B,EAAQ,KAAKC,IAAQ,MAAQF,EAAW,QAAQC,CAAG,CAAC;EACtD;CACF,CAAC;AACH"}
|
package/dist/tabula.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var TabulaError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},TabulaParseError=class extends TabulaError{},TabulaRowError=class extends TabulaError{issues;line;constructor(e,t){super(`row at line ${e} failed validation`,{cause:t}),this.line=e,this.issues=t}},TabulaValidationError=class extends TabulaError{errors;constructor(e,t=`${e.length} invalid row(s)`){super(t,{cause:e}),this.errors=e}},e=`\r
|
|
2
|
-
`;function detectDelimiter(e){let t=e.slice(0,2048).split(/[\n\r]/,1)[0]??``,n=`,`,r=-1;for(let e of[` `,`;`,`,`]){let i=0,a=!1;for(let n=0;n<t.length;n+=1){let r=t[n];r===`"`?a=!a:r===e&&!a&&(i+=1)}i>r&&(n=e,r=i)}return r>0?n:`,`}var CsvScanner=class{delimiter;skipEmptyLines;field=``;row=[];inQuotes=!1;fieldWasQuoted=!1;pendingCr=!1;started=!1;line=1;rowLine=1;constructor(e,t
|
|
3
|
-
`)||(this.started||
|
|
4
|
-
`),this.inQuotes?(
|
|
5
|
-
`&&(this.line+=1),
|
|
6
|
-
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=
|
|
7
|
-
`)||e.includes(`\r`)||e!==e.trim()}function serializeCell(e,t,n){let r=e==null?``:String(e);return!n&&!needsQuotes(r,t)?r:`"${r.replaceAll(`"`,`""`)}"`}function toCsv(t,n={}){let r=n.delimiter
|
|
2
|
+
`;function assertSingleChar(e){if(e.length!==1)throw new TabulaParseError(`The delimiter must be a single character, got "${e}".`);return e}function detectDelimiter(e){let t=e.slice(0,2048).split(/[\n\r]/,1)[0]??``,n=`,`,r=-1;for(let e of[` `,`;`,`,`]){let i=0,a=!1;for(let n=0;n<t.length;n+=1){let r=t[n];r===`"`?a=!a:r===e&&!a&&(i+=1)}i>r&&(n=e,r=i)}return r>0?n:`,`}var CsvScanner=class{delimiter;skipEmptyLines;buffer=``;field=``;row=[];inQuotes=!1;fieldWasQuoted=!1;pendingCr=!1;started=!1;line=1;rowLine=1;constructor(e={}){this.delimiter=e.delimiter===void 0?void 0:assertSingleChar(e.delimiter),this.skipEmptyLines=e.skipEmptyLines!==!1}push(e,t){if(this.delimiter===void 0){if(this.buffer+=e,!/[\n\r]/.test(this.buffer))return;this.delimiter=detectDelimiter(this.buffer),e=this.buffer,this.buffer=``}this.scan(e,t)}finish(e){if(this.buffer!==``){let t=this.buffer;this.buffer=``,this.delimiter=this.delimiter??detectDelimiter(t),this.scan(t,e)}(this.field!==``||this.row.length>0||this.fieldWasQuoted)&&this.endRow(e),this.pendingCr=!1}scan(e,t){let n=this.delimiter;for(let r=0;r<e.length;r+=1){let i=e[r];this.pendingCr&&(this.pendingCr=!1,i===`
|
|
3
|
+
`)||(this.started||r!==0||i!==``)&&(this.started=!0,i===`\r`&&(this.pendingCr=!0,i=`
|
|
4
|
+
`),this.inQuotes?(i===`
|
|
5
|
+
`&&(this.line+=1),i===`"`?e[r+1]===`"`?(this.field+=`"`,r+=1):this.inQuotes=!1:this.field+=i):i===`"`&&!this.fieldWasQuoted&&this.field===``?(this.inQuotes=!0,this.fieldWasQuoted=!0):i===`"`?this.field+=`"`:i===n?(this.row.push(this.field),this.field=``,this.fieldWasQuoted=!1):i===`
|
|
6
|
+
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=i)}}endRow(e){this.row.push(this.field),this.field=``,this.row.length===1&&this.row[0]===``&&this.skipEmptyLines||e(this.row,this.rowLine),this.row=[],this.line+=1,this.rowLine=this.line}};function parseGrid(e,t={}){let n=new CsvScanner(t),r=[];return n.push(e,e=>r.push(e)),n.finish(e=>r.push(e)),r}function parseTrackedRecords(e,t={}){if(t.headers!==void 0&&t.headers.length===0)throw new TabulaParseError(`headers must name at least one column`);let n=new CsvScanner(t),r=[];if(n.push(e,(e,t)=>r.push([e,t])),n.finish((e,t)=>r.push([e,t])),r.length===0)return[];let i=t.headers??r[0][0];return(t.headers===void 0?r.slice(1):r).map(([e,t])=>{let n={};return i.forEach((t,r)=>{let i=e[r];i!==void 0&&(n[t]=i)}),{line:t,record:n}})}function parseRecords(e,t={}){return parseTrackedRecords(e,t).map(({record:e})=>e)}function needsQuotes(e,t){return e.includes(t)||e.includes(`"`)||e.includes(`
|
|
7
|
+
`)||e.includes(`\r`)||e!==e.trim()}function serializeCell(e,t,n){let r=e==null?``:String(e);return!n&&!needsQuotes(r,t)?r:`"${r.replaceAll(`"`,`""`)}"`}function toCsv(t,n={}){let r=assertSingleChar(n.delimiter??`,`),i=n.newline??e,a=n.quoteAll===!0,o=[];for(let e of t)o.push(e.map(e=>serializeCell(e,r,a)).join(r));return o.join(i)+(o.length>0?i:``)}function recordsToCsv(e,t={}){let n=[...e],r=t.columns??[...new Set(n.flatMap(e=>Object.keys(e)))],i=[r];for(let e of n)i.push(r.map(t=>e[t]??``));return toCsv(i,t)}function createCsvGridStream(e={}){let t=new CsvScanner(e);return new TransformStream({flush(e){t.finish(t=>e.enqueue(t))},transform(e,n){t.push(e,e=>n.enqueue(e))}})}function parseTypedRecords(e,t,n={}){let r=[],i=[];for(let{line:a,record:o}of parseTrackedRecords(e,n)){let e=t[`~standard`].validate(o);if(e instanceof Promise)throw new TabulaParseError(`async schemas are not supported; use a sync schema`);e.issues===void 0?r.push(e.value):i.push(new TabulaRowError(a,e.issues))}if(i.length>0)throw new TabulaValidationError(i);return r}exports.CsvScanner=CsvScanner,exports.TabulaError=TabulaError,exports.TabulaParseError=TabulaParseError,exports.TabulaRowError=TabulaRowError,exports.TabulaValidationError=TabulaValidationError,exports.createCsvGridStream=createCsvGridStream,exports.parseGrid=parseGrid,exports.parseRecords=parseRecords,exports.parseTypedRecords=parseTypedRecords,exports.recordsToCsv=recordsToCsv,exports.toCsv=toCsv;
|
|
8
8
|
//# sourceMappingURL=tabula.cjs.map
|
package/dist/tabula.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabula.cjs","names":["message","options","line","issues","errors","DEFAULT_NEWLINE","firstLine","text","best","bestCount","candidate","count","quoted","i","char","delimiter","skipEmptyLines","onRow","scanner","rows","headers","record","cell","cells","index","header","row","value","raw","quoteAll","newline","lines","list","records","columns","column","pending","active","controller","chunk","values","result","schema"],"sources":["../src/errors.ts","../src/csv.ts","../src/stream.ts","../src/typed.ts"],"sourcesContent":["/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */\nexport class TabulaError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */\nexport class TabulaParseError extends TabulaError {}\n\n/** One row failed schema validation; `line` is 1-based over physical document lines. */\nexport class TabulaRowError extends TabulaError {\n readonly issues: readonly unknown[];\n readonly line: number;\n\n constructor(line: number, issues: readonly unknown[]) {\n super(`row at line ${line} failed validation`, { cause: issues });\n this.line = line;\n this.issues = issues;\n }\n}\n\n/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */\nexport class TabulaValidationError extends TabulaError {\n readonly errors: readonly TabulaRowError[];\n\n constructor(errors: readonly TabulaRowError[], message = `${errors.length} invalid row(s)`) {\n super(message, { cause: errors });\n this.errors = errors;\n }\n}\n","import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nexport function detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows. Rows may span\n * chunks; `finish()` flushes the pending row. Handles BOM, LF/CRLF/CR, quoted\n * fields with embedded delimiters, newlines and `\"\"` escapes.\n */\nexport class CsvScanner {\n private readonly delimiter: string;\n private readonly skipEmptyLines: boolean;\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(delimiter: string, skipEmptyLines = true) {\n this.delimiter = delimiter;\n this.skipEmptyLines = skipEmptyLines;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === this.delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nfunction scan(text: string, options: TabulaParseOptions, onRow: (row: string[], line: number) => void): void {\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n scanner.push(text, onRow);\n scanner.finish(onRow);\n}\n\n/** A row paired with the 1-based physical line it started on. */\nexport type LineRow = { line: number; record: Record<string, string> };\n\n/** Parse into records while keeping each row's physical start line (for error reporting). */\nexport function parseRecordsWithLines(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const rows: { cells: string[]; line: number }[] = [];\n scan(text, options, (cells, line) => rows.push({ cells, line }));\n if (rows.length === 0) return [];\n const headers = options.headers ?? ((rows[0] as { cells: string[] }).cells as readonly string[]);\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(({ cells, line }) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const rows: string[][] = [];\n scan(text, options, (row) => rows.push(row));\n return rows;\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseRecordsWithLines(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = options.delimiter ?? ',';\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/** Serialize records to CSV with a header row; `columns` overrides column order. */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n","import { CsvScanner, detectDelimiter } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out — rows spanning chunk boundaries are assembled internally,\n * and the delimiter is auto-detected once the first line is available (unless\n * pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n let delimiter: string | undefined = options.delimiter;\n let scanner: CsvScanner | undefined =\n delimiter === undefined ? undefined : new CsvScanner(delimiter, options.skipEmptyLines !== false);\n let pending = '';\n const ensureScanner = (): CsvScanner => {\n if (scanner === undefined) {\n delimiter = detectDelimiter(pending);\n scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n }\n return scanner;\n };\n return new TransformStream<string, string[]>({\n flush(controller) {\n const active = ensureScanner();\n if (pending !== '') active.push(pending, (row) => controller.enqueue(row));\n active.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n if (scanner === undefined) {\n pending += chunk;\n // Wait for the first full line so auto-detection sees a complete header.\n if (!/[\\n\\r]/.test(pending)) return;\n ensureScanner().push(pending, (row) => controller.enqueue(row));\n pending = '';\n return;\n }\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n","import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseRecordsWithLines } from './csv';\nimport { TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseRecordsWithLines(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TypeError('tabula: async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tabula.cjs","names":["message","options","line","issues","errors","DEFAULT_NEWLINE","delimiter","firstLine","text","best","bestCount","candidate","count","quoted","i","char","onRow","scanner","rows","row","cells","headers","record","cell","index","header","value","raw","quoteAll","newline","lines","list","records","columns","column","controller","chunk","values","result","schema"],"sources":["../src/errors.ts","../src/csv.ts","../src/stream.ts","../src/typed.ts"],"sourcesContent":["/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */\nexport class TabulaError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */\nexport class TabulaParseError extends TabulaError {}\n\n/** One row failed schema validation; `line` is 1-based over physical document lines. */\nexport class TabulaRowError extends TabulaError {\n readonly issues: readonly unknown[];\n readonly line: number;\n\n constructor(line: number, issues: readonly unknown[]) {\n super(`row at line ${line} failed validation`, { cause: issues });\n this.line = line;\n this.issues = issues;\n }\n}\n\n/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */\nexport class TabulaValidationError extends TabulaError {\n readonly errors: readonly TabulaRowError[];\n\n constructor(errors: readonly TabulaRowError[], message = `${errors.length} invalid row(s)`) {\n super(message, { cause: errors });\n this.errors = errors;\n }\n}\n","import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** A delimiter the scanner can match: exactly one character. */\nfunction assertSingleChar(delimiter: string): string {\n if (delimiter.length !== 1) {\n throw new TabulaParseError(`The delimiter must be a single character, got \"${delimiter}\".`);\n }\n return delimiter;\n}\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nfunction detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows with the\n * 1-based physical line each started on. Rows may span chunks; `finish()`\n * flushes the pending row. Handles BOM, LF/CRLF/CR, quoted fields with\n * embedded delimiters, newlines and `\"\"` escapes.\n *\n * Without a pinned `delimiter` the scanner accumulates until the first line\n * completes, auto-detects, and starts emitting rows.\n */\nexport class CsvScanner {\n private delimiter: string | undefined;\n private readonly skipEmptyLines: boolean;\n private buffer = '';\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(options: TabulaParseOptions = {}) {\n this.delimiter = options.delimiter === undefined ? undefined : assertSingleChar(options.delimiter);\n this.skipEmptyLines = options.skipEmptyLines !== false;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n if (this.delimiter === undefined) {\n this.buffer += text;\n // Auto-detection needs a complete first line; hold the chunks until then.\n if (!/[\\n\\r]/.test(this.buffer)) return;\n this.delimiter = detectDelimiter(this.buffer);\n text = this.buffer;\n this.buffer = '';\n }\n this.scan(text, onRow);\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.buffer !== '') {\n const text = this.buffer;\n this.buffer = '';\n this.delimiter = this.delimiter ?? detectDelimiter(text);\n this.scan(text, onRow);\n }\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private scan(text: string, onRow: (row: string[], line: number) => void): void {\n const delimiter = this.delimiter as string;\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const scanner = new CsvScanner(options);\n const rows: string[][] = [];\n scanner.push(text, (row) => rows.push(row));\n scanner.finish((row) => rows.push(row));\n return rows;\n}\n\n/** A row paired with the 1-based physical line it started on. */\ntype LineRow = { line: number; record: Record<string, string> };\n\n/**\n * Parse into records while keeping each row's physical start line: the shared\n * core of {@link parseRecords} and the typed reader. Headers default to the\n * first row; explicit `headers` make it headerless. Extra cells are dropped,\n * short rows leave keys off.\n */\nexport function parseTrackedRecords(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const scanner = new CsvScanner(options);\n const rows: [cells: string[], line: number][] = [];\n scanner.push(text, (cells, line) => rows.push([cells, line]));\n scanner.finish((cells, line) => rows.push([cells, line]));\n if (rows.length === 0) return [];\n const headers = options.headers ?? rows[0][0];\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(([cells, line]) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseTrackedRecords(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = assertSingleChar(options.delimiter ?? ',');\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/**\n * Serialize records to CSV with a header row. Columns are the union of every\n * record's keys in first-seen order unless `columns` overrides the order.\n */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n","import { CsvScanner } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out. The {@link CsvScanner} assembles rows across chunk\n * boundaries and auto-detects the delimiter once the first line is available\n * (unless pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n const scanner = new CsvScanner(options);\n return new TransformStream<string, string[]>({\n flush(controller) {\n scanner.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n","import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseTrackedRecords } from './csv';\nimport { TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseTrackedRecords(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TabulaParseError('async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"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"}
|
package/dist/tabula.iife.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
var Tabula=(function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var TabulaError=class extends Error{constructor(e,t){super(e,t),this.name=new.target.name,Object.setPrototypeOf(this,new.target.prototype)}},TabulaParseError=class extends TabulaError{},TabulaRowError=class extends TabulaError{issues;line;constructor(e,t){super(`row at line ${e} failed validation`,{cause:t}),this.line=e,this.issues=t}},TabulaValidationError=class extends TabulaError{errors;constructor(e,t=`${e.length} invalid row(s)`){super(t,{cause:e}),this.errors=e}},t=`\r
|
|
2
|
-
`;function detectDelimiter(e){let t=e.slice(0,2048).split(/[\n\r]/,1)[0]??``,n=`,`,r=-1;for(let e of[` `,`;`,`,`]){let i=0,a=!1;for(let n=0;n<t.length;n+=1){let r=t[n];r===`"`?a=!a:r===e&&!a&&(i+=1)}i>r&&(n=e,r=i)}return r>0?n:`,`}var CsvScanner=class{delimiter;skipEmptyLines;field=``;row=[];inQuotes=!1;fieldWasQuoted=!1;pendingCr=!1;started=!1;line=1;rowLine=1;constructor(e,t
|
|
3
|
-
`)||(this.started||
|
|
4
|
-
`),this.inQuotes?(
|
|
5
|
-
`&&(this.line+=1),
|
|
6
|
-
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=
|
|
7
|
-
`)||e.includes(`\r`)||e!==e.trim()}function serializeCell(e,t,n){let r=e==null?``:String(e);return!n&&!needsQuotes(r,t)?r:`"${r.replaceAll(`"`,`""`)}"`}function toCsv(e,n={}){let r=n.delimiter
|
|
2
|
+
`;function assertSingleChar(e){if(e.length!==1)throw new TabulaParseError(`The delimiter must be a single character, got "${e}".`);return e}function detectDelimiter(e){let t=e.slice(0,2048).split(/[\n\r]/,1)[0]??``,n=`,`,r=-1;for(let e of[` `,`;`,`,`]){let i=0,a=!1;for(let n=0;n<t.length;n+=1){let r=t[n];r===`"`?a=!a:r===e&&!a&&(i+=1)}i>r&&(n=e,r=i)}return r>0?n:`,`}var CsvScanner=class{delimiter;skipEmptyLines;buffer=``;field=``;row=[];inQuotes=!1;fieldWasQuoted=!1;pendingCr=!1;started=!1;line=1;rowLine=1;constructor(e={}){this.delimiter=e.delimiter===void 0?void 0:assertSingleChar(e.delimiter),this.skipEmptyLines=e.skipEmptyLines!==!1}push(e,t){if(this.delimiter===void 0){if(this.buffer+=e,!/[\n\r]/.test(this.buffer))return;this.delimiter=detectDelimiter(this.buffer),e=this.buffer,this.buffer=``}this.scan(e,t)}finish(e){if(this.buffer!==``){let t=this.buffer;this.buffer=``,this.delimiter=this.delimiter??detectDelimiter(t),this.scan(t,e)}(this.field!==``||this.row.length>0||this.fieldWasQuoted)&&this.endRow(e),this.pendingCr=!1}scan(e,t){let n=this.delimiter;for(let r=0;r<e.length;r+=1){let i=e[r];this.pendingCr&&(this.pendingCr=!1,i===`
|
|
3
|
+
`)||(this.started||r!==0||i!==``)&&(this.started=!0,i===`\r`&&(this.pendingCr=!0,i=`
|
|
4
|
+
`),this.inQuotes?(i===`
|
|
5
|
+
`&&(this.line+=1),i===`"`?e[r+1]===`"`?(this.field+=`"`,r+=1):this.inQuotes=!1:this.field+=i):i===`"`&&!this.fieldWasQuoted&&this.field===``?(this.inQuotes=!0,this.fieldWasQuoted=!0):i===`"`?this.field+=`"`:i===n?(this.row.push(this.field),this.field=``,this.fieldWasQuoted=!1):i===`
|
|
6
|
+
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=i)}}endRow(e){this.row.push(this.field),this.field=``,this.row.length===1&&this.row[0]===``&&this.skipEmptyLines||e(this.row,this.rowLine),this.row=[],this.line+=1,this.rowLine=this.line}};function parseGrid(e,t={}){let n=new CsvScanner(t),r=[];return n.push(e,e=>r.push(e)),n.finish(e=>r.push(e)),r}function parseTrackedRecords(e,t={}){if(t.headers!==void 0&&t.headers.length===0)throw new TabulaParseError(`headers must name at least one column`);let n=new CsvScanner(t),r=[];if(n.push(e,(e,t)=>r.push([e,t])),n.finish((e,t)=>r.push([e,t])),r.length===0)return[];let i=t.headers??r[0][0];return(t.headers===void 0?r.slice(1):r).map(([e,t])=>{let n={};return i.forEach((t,r)=>{let i=e[r];i!==void 0&&(n[t]=i)}),{line:t,record:n}})}function parseRecords(e,t={}){return parseTrackedRecords(e,t).map(({record:e})=>e)}function needsQuotes(e,t){return e.includes(t)||e.includes(`"`)||e.includes(`
|
|
7
|
+
`)||e.includes(`\r`)||e!==e.trim()}function serializeCell(e,t,n){let r=e==null?``:String(e);return!n&&!needsQuotes(r,t)?r:`"${r.replaceAll(`"`,`""`)}"`}function toCsv(e,n={}){let r=assertSingleChar(n.delimiter??`,`),i=n.newline??t,a=n.quoteAll===!0,o=[];for(let t of e)o.push(t.map(e=>serializeCell(e,r,a)).join(r));return o.join(i)+(o.length>0?i:``)}function recordsToCsv(e,t={}){let n=[...e],r=t.columns??[...new Set(n.flatMap(e=>Object.keys(e)))],i=[r];for(let e of n)i.push(r.map(t=>e[t]??``));return toCsv(i,t)}function createCsvGridStream(e={}){let t=new CsvScanner(e);return new TransformStream({flush(e){t.finish(t=>e.enqueue(t))},transform(e,n){t.push(e,e=>n.enqueue(e))}})}function parseTypedRecords(e,t,n={}){let r=[],i=[];for(let{line:a,record:o}of parseTrackedRecords(e,n)){let e=t[`~standard`].validate(o);if(e instanceof Promise)throw new TabulaParseError(`async schemas are not supported; use a sync schema`);e.issues===void 0?r.push(e.value):i.push(new TabulaRowError(a,e.issues))}if(i.length>0)throw new TabulaValidationError(i);return r}return e.CsvScanner=CsvScanner,e.TabulaError=TabulaError,e.TabulaParseError=TabulaParseError,e.TabulaRowError=TabulaRowError,e.TabulaValidationError=TabulaValidationError,e.createCsvGridStream=createCsvGridStream,e.parseGrid=parseGrid,e.parseRecords=parseRecords,e.parseTypedRecords=parseTypedRecords,e.recordsToCsv=recordsToCsv,e.toCsv=toCsv,e})({});
|
|
8
8
|
//# sourceMappingURL=tabula.iife.js.map
|
package/dist/tabula.iife.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabula.iife.js","names":["exports","message","options","line","issues","errors","DEFAULT_NEWLINE","firstLine","text","best","bestCount","candidate","count","quoted","i","char","delimiter","skipEmptyLines","onRow","scanner","rows","headers","record","cell","cells","index","header","row","value","raw","quoteAll","newline","lines","list","records","columns","column","pending","active","controller","chunk","values","result","schema"],"sources":["../src/errors.ts","../src/csv.ts","../src/stream.ts","../src/typed.ts"],"sourcesContent":["/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */\nexport class TabulaError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */\nexport class TabulaParseError extends TabulaError {}\n\n/** One row failed schema validation; `line` is 1-based over physical document lines. */\nexport class TabulaRowError extends TabulaError {\n readonly issues: readonly unknown[];\n readonly line: number;\n\n constructor(line: number, issues: readonly unknown[]) {\n super(`row at line ${line} failed validation`, { cause: issues });\n this.line = line;\n this.issues = issues;\n }\n}\n\n/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */\nexport class TabulaValidationError extends TabulaError {\n readonly errors: readonly TabulaRowError[];\n\n constructor(errors: readonly TabulaRowError[], message = `${errors.length} invalid row(s)`) {\n super(message, { cause: errors });\n this.errors = errors;\n }\n}\n","import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nexport function detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows. Rows may span\n * chunks; `finish()` flushes the pending row. Handles BOM, LF/CRLF/CR, quoted\n * fields with embedded delimiters, newlines and `\"\"` escapes.\n */\nexport class CsvScanner {\n private readonly delimiter: string;\n private readonly skipEmptyLines: boolean;\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(delimiter: string, skipEmptyLines = true) {\n this.delimiter = delimiter;\n this.skipEmptyLines = skipEmptyLines;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === this.delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nfunction scan(text: string, options: TabulaParseOptions, onRow: (row: string[], line: number) => void): void {\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n scanner.push(text, onRow);\n scanner.finish(onRow);\n}\n\n/** A row paired with the 1-based physical line it started on. */\nexport type LineRow = { line: number; record: Record<string, string> };\n\n/** Parse into records while keeping each row's physical start line (for error reporting). */\nexport function parseRecordsWithLines(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const rows: { cells: string[]; line: number }[] = [];\n scan(text, options, (cells, line) => rows.push({ cells, line }));\n if (rows.length === 0) return [];\n const headers = options.headers ?? ((rows[0] as { cells: string[] }).cells as readonly string[]);\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(({ cells, line }) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const rows: string[][] = [];\n scan(text, options, (row) => rows.push(row));\n return rows;\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseRecordsWithLines(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = options.delimiter ?? ',';\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/** Serialize records to CSV with a header row; `columns` overrides column order. */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n","import { CsvScanner, detectDelimiter } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out — rows spanning chunk boundaries are assembled internally,\n * and the delimiter is auto-detected once the first line is available (unless\n * pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n let delimiter: string | undefined = options.delimiter;\n let scanner: CsvScanner | undefined =\n delimiter === undefined ? undefined : new CsvScanner(delimiter, options.skipEmptyLines !== false);\n let pending = '';\n const ensureScanner = (): CsvScanner => {\n if (scanner === undefined) {\n delimiter = detectDelimiter(pending);\n scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n }\n return scanner;\n };\n return new TransformStream<string, string[]>({\n flush(controller) {\n const active = ensureScanner();\n if (pending !== '') active.push(pending, (row) => controller.enqueue(row));\n active.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n if (scanner === undefined) {\n pending += chunk;\n // Wait for the first full line so auto-detection sees a complete header.\n if (!/[\\n\\r]/.test(pending)) return;\n ensureScanner().push(pending, (row) => controller.enqueue(row));\n pending = '';\n return;\n }\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n","import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseRecordsWithLines } from './csv';\nimport { TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseRecordsWithLines(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TypeError('tabula: async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tabula.iife.js","names":["exports","message","options","line","issues","errors","DEFAULT_NEWLINE","delimiter","firstLine","text","best","bestCount","candidate","count","quoted","i","char","onRow","scanner","rows","row","cells","headers","record","cell","index","header","value","raw","quoteAll","newline","lines","list","records","columns","column","controller","chunk","values","result","schema"],"sources":["../src/errors.ts","../src/csv.ts","../src/stream.ts","../src/typed.ts"],"sourcesContent":["/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */\nexport class TabulaError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */\nexport class TabulaParseError extends TabulaError {}\n\n/** One row failed schema validation; `line` is 1-based over physical document lines. */\nexport class TabulaRowError extends TabulaError {\n readonly issues: readonly unknown[];\n readonly line: number;\n\n constructor(line: number, issues: readonly unknown[]) {\n super(`row at line ${line} failed validation`, { cause: issues });\n this.line = line;\n this.issues = issues;\n }\n}\n\n/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */\nexport class TabulaValidationError extends TabulaError {\n readonly errors: readonly TabulaRowError[];\n\n constructor(errors: readonly TabulaRowError[], message = `${errors.length} invalid row(s)`) {\n super(message, { cause: errors });\n this.errors = errors;\n }\n}\n","import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** A delimiter the scanner can match: exactly one character. */\nfunction assertSingleChar(delimiter: string): string {\n if (delimiter.length !== 1) {\n throw new TabulaParseError(`The delimiter must be a single character, got \"${delimiter}\".`);\n }\n return delimiter;\n}\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nfunction detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows with the\n * 1-based physical line each started on. Rows may span chunks; `finish()`\n * flushes the pending row. Handles BOM, LF/CRLF/CR, quoted fields with\n * embedded delimiters, newlines and `\"\"` escapes.\n *\n * Without a pinned `delimiter` the scanner accumulates until the first line\n * completes, auto-detects, and starts emitting rows.\n */\nexport class CsvScanner {\n private delimiter: string | undefined;\n private readonly skipEmptyLines: boolean;\n private buffer = '';\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(options: TabulaParseOptions = {}) {\n this.delimiter = options.delimiter === undefined ? undefined : assertSingleChar(options.delimiter);\n this.skipEmptyLines = options.skipEmptyLines !== false;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n if (this.delimiter === undefined) {\n this.buffer += text;\n // Auto-detection needs a complete first line; hold the chunks until then.\n if (!/[\\n\\r]/.test(this.buffer)) return;\n this.delimiter = detectDelimiter(this.buffer);\n text = this.buffer;\n this.buffer = '';\n }\n this.scan(text, onRow);\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.buffer !== '') {\n const text = this.buffer;\n this.buffer = '';\n this.delimiter = this.delimiter ?? detectDelimiter(text);\n this.scan(text, onRow);\n }\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private scan(text: string, onRow: (row: string[], line: number) => void): void {\n const delimiter = this.delimiter as string;\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const scanner = new CsvScanner(options);\n const rows: string[][] = [];\n scanner.push(text, (row) => rows.push(row));\n scanner.finish((row) => rows.push(row));\n return rows;\n}\n\n/** A row paired with the 1-based physical line it started on. */\ntype LineRow = { line: number; record: Record<string, string> };\n\n/**\n * Parse into records while keeping each row's physical start line: the shared\n * core of {@link parseRecords} and the typed reader. Headers default to the\n * first row; explicit `headers` make it headerless. Extra cells are dropped,\n * short rows leave keys off.\n */\nexport function parseTrackedRecords(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const scanner = new CsvScanner(options);\n const rows: [cells: string[], line: number][] = [];\n scanner.push(text, (cells, line) => rows.push([cells, line]));\n scanner.finish((cells, line) => rows.push([cells, line]));\n if (rows.length === 0) return [];\n const headers = options.headers ?? rows[0][0];\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(([cells, line]) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseTrackedRecords(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = assertSingleChar(options.delimiter ?? ',');\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/**\n * Serialize records to CSV with a header row. Columns are the union of every\n * record's keys in first-seen order unless `columns` overrides the order.\n */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n","import { CsvScanner } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out. The {@link CsvScanner} assembles rows across chunk\n * boundaries and auto-detects the delimiter once the first line is available\n * (unless pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n const scanner = new CsvScanner(options);\n return new TransformStream<string, string[]>({\n flush(controller) {\n scanner.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n","import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseTrackedRecords } from './csv';\nimport { TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseTrackedRecords(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TabulaParseError('async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"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"}
|
package/dist/tabula.js
CHANGED
|
@@ -15,6 +15,10 @@ var TabulaError = class extends Error {
|
|
|
15
15
|
super(t, { cause: e }), this.errors = e;
|
|
16
16
|
}
|
|
17
17
|
}, e = "\r\n";
|
|
18
|
+
function assertSingleChar(e) {
|
|
19
|
+
if (e.length !== 1) throw new TabulaParseError(`The delimiter must be a single character, got "${e}".`);
|
|
20
|
+
return e;
|
|
21
|
+
}
|
|
18
22
|
function detectDelimiter(e) {
|
|
19
23
|
let t = e.slice(0, 2048).split(/[\n\r]/, 1)[0] ?? "", n = ",", r = -1;
|
|
20
24
|
for (let e of [
|
|
@@ -34,6 +38,7 @@ function detectDelimiter(e) {
|
|
|
34
38
|
var CsvScanner = class {
|
|
35
39
|
delimiter;
|
|
36
40
|
skipEmptyLines;
|
|
41
|
+
buffer = "";
|
|
37
42
|
field = "";
|
|
38
43
|
row = [];
|
|
39
44
|
inQuotes = !1;
|
|
@@ -42,37 +47,46 @@ var CsvScanner = class {
|
|
|
42
47
|
started = !1;
|
|
43
48
|
line = 1;
|
|
44
49
|
rowLine = 1;
|
|
45
|
-
constructor(e
|
|
46
|
-
this.delimiter = e, this.skipEmptyLines =
|
|
50
|
+
constructor(e = {}) {
|
|
51
|
+
this.delimiter = e.delimiter === void 0 ? void 0 : assertSingleChar(e.delimiter), this.skipEmptyLines = e.skipEmptyLines !== !1;
|
|
47
52
|
}
|
|
48
53
|
push(e, t) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
this.
|
|
54
|
+
if (this.delimiter === void 0) {
|
|
55
|
+
if (this.buffer += e, !/[\n\r]/.test(this.buffer)) return;
|
|
56
|
+
this.delimiter = detectDelimiter(this.buffer), e = this.buffer, this.buffer = "";
|
|
52
57
|
}
|
|
58
|
+
this.scan(e, t);
|
|
53
59
|
}
|
|
54
60
|
finish(e) {
|
|
61
|
+
if (this.buffer !== "") {
|
|
62
|
+
let t = this.buffer;
|
|
63
|
+
this.buffer = "", this.delimiter = this.delimiter ?? detectDelimiter(t), this.scan(t, e);
|
|
64
|
+
}
|
|
55
65
|
(this.field !== "" || this.row.length > 0 || this.fieldWasQuoted) && this.endRow(e), this.pendingCr = !1;
|
|
56
66
|
}
|
|
67
|
+
scan(e, t) {
|
|
68
|
+
let n = this.delimiter;
|
|
69
|
+
for (let r = 0; r < e.length; r += 1) {
|
|
70
|
+
let i = e[r];
|
|
71
|
+
this.pendingCr && (this.pendingCr = !1, i === "\n") || (this.started || r !== 0 || i !== "") && (this.started = !0, i === "\r" && (this.pendingCr = !0, i = "\n"), this.inQuotes ? (i === "\n" && (this.line += 1), i === "\"" ? e[r + 1] === "\"" ? (this.field += "\"", r += 1) : this.inQuotes = !1 : this.field += i) : i === "\"" && !this.fieldWasQuoted && this.field === "" ? (this.inQuotes = !0, this.fieldWasQuoted = !0) : i === "\"" ? this.field += "\"" : i === n ? (this.row.push(this.field), this.field = "", this.fieldWasQuoted = !1) : i === "\n" ? (this.endRow(t), this.fieldWasQuoted = !1) : this.field += i);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
57
74
|
endRow(e) {
|
|
58
75
|
this.row.push(this.field), this.field = "", this.row.length === 1 && this.row[0] === "" && this.skipEmptyLines || e(this.row, this.rowLine), this.row = [], this.line += 1, this.rowLine = this.line;
|
|
59
76
|
}
|
|
60
77
|
};
|
|
61
|
-
function
|
|
62
|
-
let
|
|
63
|
-
r.push(e, n)
|
|
78
|
+
function parseGrid(e, t = {}) {
|
|
79
|
+
let n = new CsvScanner(t), r = [];
|
|
80
|
+
return n.push(e, (e) => r.push(e)), n.finish((e) => r.push(e)), r;
|
|
64
81
|
}
|
|
65
|
-
function
|
|
82
|
+
function parseTrackedRecords(e, t = {}) {
|
|
66
83
|
if (t.headers !== void 0 && t.headers.length === 0) throw new TabulaParseError("headers must name at least one column");
|
|
67
|
-
let n = [];
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
})), n.length === 0) return [];
|
|
72
|
-
let r = t.headers ?? n[0].cells;
|
|
73
|
-
return (t.headers === void 0 ? n.slice(1) : n).map(({ cells: e, line: t }) => {
|
|
84
|
+
let n = new CsvScanner(t), r = [];
|
|
85
|
+
if (n.push(e, (e, t) => r.push([e, t])), n.finish((e, t) => r.push([e, t])), r.length === 0) return [];
|
|
86
|
+
let i = t.headers ?? r[0][0];
|
|
87
|
+
return (t.headers === void 0 ? r.slice(1) : r).map(([e, t]) => {
|
|
74
88
|
let n = {};
|
|
75
|
-
return
|
|
89
|
+
return i.forEach((t, r) => {
|
|
76
90
|
let i = e[r];
|
|
77
91
|
i !== void 0 && (n[t] = i);
|
|
78
92
|
}), {
|
|
@@ -81,12 +95,8 @@ function parseRecordsWithLines(e, t = {}) {
|
|
|
81
95
|
};
|
|
82
96
|
});
|
|
83
97
|
}
|
|
84
|
-
function parseGrid(e, t = {}) {
|
|
85
|
-
let n = [];
|
|
86
|
-
return scan(e, t, (e) => n.push(e)), n;
|
|
87
|
-
}
|
|
88
98
|
function parseRecords(e, t = {}) {
|
|
89
|
-
return
|
|
99
|
+
return parseTrackedRecords(e, t).map(({ record: e }) => e);
|
|
90
100
|
}
|
|
91
101
|
function needsQuotes(e, t) {
|
|
92
102
|
return e.includes(t) || e.includes("\"") || e.includes("\n") || e.includes("\r") || e !== e.trim();
|
|
@@ -96,7 +106,7 @@ function serializeCell(e, t, n) {
|
|
|
96
106
|
return !n && !needsQuotes(r, t) ? r : `"${r.replaceAll("\"", "\"\"")}"`;
|
|
97
107
|
}
|
|
98
108
|
function toCsv(t, n = {}) {
|
|
99
|
-
let r = n.delimiter ?? ",", i = n.newline ?? e, a = n.quoteAll === !0, o = [];
|
|
109
|
+
let r = assertSingleChar(n.delimiter ?? ","), i = n.newline ?? e, a = n.quoteAll === !0, o = [];
|
|
100
110
|
for (let e of t) o.push(e.map((e) => serializeCell(e, r, a)).join(r));
|
|
101
111
|
return o.join(i) + (o.length > 0 ? i : "");
|
|
102
112
|
}
|
|
@@ -107,31 +117,27 @@ function recordsToCsv(e, t = {}) {
|
|
|
107
117
|
}
|
|
108
118
|
// src/stream.ts
|
|
109
119
|
function createCsvGridStream(e = {}) {
|
|
110
|
-
let t =
|
|
120
|
+
let t = new CsvScanner(e);
|
|
111
121
|
return new TransformStream({
|
|
112
122
|
flush(e) {
|
|
113
|
-
|
|
114
|
-
r !== "" && t.push(r, (t) => e.enqueue(t)), t.finish((t) => e.enqueue(t));
|
|
123
|
+
t.finish((t) => e.enqueue(t));
|
|
115
124
|
},
|
|
116
|
-
transform(e,
|
|
117
|
-
|
|
118
|
-
if (r += e, !/[\n\r]/.test(r)) return;
|
|
119
|
-
ensureScanner().push(r, (e) => t.enqueue(e)), r = "";
|
|
120
|
-
} else n.push(e, (e) => t.enqueue(e));
|
|
125
|
+
transform(e, n) {
|
|
126
|
+
t.push(e, (e) => n.enqueue(e));
|
|
121
127
|
}
|
|
122
128
|
});
|
|
123
129
|
}
|
|
124
130
|
// src/typed.ts
|
|
125
131
|
function parseTypedRecords(e, t, n = {}) {
|
|
126
132
|
let r = [], i = [];
|
|
127
|
-
for (let { line: a, record: o } of
|
|
133
|
+
for (let { line: a, record: o } of parseTrackedRecords(e, n)) {
|
|
128
134
|
let e = t["~standard"].validate(o);
|
|
129
|
-
if (e instanceof Promise) throw
|
|
135
|
+
if (e instanceof Promise) throw new TabulaParseError("async schemas are not supported; use a sync schema");
|
|
130
136
|
e.issues === void 0 ? r.push(e.value) : i.push(new TabulaRowError(a, e.issues));
|
|
131
137
|
}
|
|
132
138
|
if (i.length > 0) throw new TabulaValidationError(i);
|
|
133
139
|
return r;
|
|
134
140
|
}
|
|
135
|
-
export { CsvScanner, TabulaError, TabulaParseError, TabulaRowError, TabulaValidationError, createCsvGridStream,
|
|
141
|
+
export { CsvScanner, TabulaError, TabulaParseError, TabulaRowError, TabulaValidationError, createCsvGridStream, parseGrid, parseRecords, parseTypedRecords, recordsToCsv, toCsv };
|
|
136
142
|
|
|
137
143
|
//# sourceMappingURL=tabula.js.map
|
package/dist/tabula.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tabula.js","names":["message","options","line","issues","errors","DEFAULT_NEWLINE","firstLine","text","best","bestCount","candidate","count","quoted","i","char","delimiter","skipEmptyLines","onRow","scanner","rows","headers","record","cell","cells","index","header","row","value","raw","quoteAll","newline","lines","list","records","columns","column","pending","active","controller","chunk","values","result","schema"],"sources":["../src/errors.ts","../src/csv.ts","../src/stream.ts","../src/typed.ts"],"sourcesContent":["/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */\nexport class TabulaError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */\nexport class TabulaParseError extends TabulaError {}\n\n/** One row failed schema validation; `line` is 1-based over physical document lines. */\nexport class TabulaRowError extends TabulaError {\n readonly issues: readonly unknown[];\n readonly line: number;\n\n constructor(line: number, issues: readonly unknown[]) {\n super(`row at line ${line} failed validation`, { cause: issues });\n this.line = line;\n this.issues = issues;\n }\n}\n\n/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */\nexport class TabulaValidationError extends TabulaError {\n readonly errors: readonly TabulaRowError[];\n\n constructor(errors: readonly TabulaRowError[], message = `${errors.length} invalid row(s)`) {\n super(message, { cause: errors });\n this.errors = errors;\n }\n}\n","import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nexport function detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows. Rows may span\n * chunks; `finish()` flushes the pending row. Handles BOM, LF/CRLF/CR, quoted\n * fields with embedded delimiters, newlines and `\"\"` escapes.\n */\nexport class CsvScanner {\n private readonly delimiter: string;\n private readonly skipEmptyLines: boolean;\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(delimiter: string, skipEmptyLines = true) {\n this.delimiter = delimiter;\n this.skipEmptyLines = skipEmptyLines;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === this.delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nfunction scan(text: string, options: TabulaParseOptions, onRow: (row: string[], line: number) => void): void {\n const delimiter = options.delimiter ?? detectDelimiter(text);\n const scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n scanner.push(text, onRow);\n scanner.finish(onRow);\n}\n\n/** A row paired with the 1-based physical line it started on. */\nexport type LineRow = { line: number; record: Record<string, string> };\n\n/** Parse into records while keeping each row's physical start line (for error reporting). */\nexport function parseRecordsWithLines(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const rows: { cells: string[]; line: number }[] = [];\n scan(text, options, (cells, line) => rows.push({ cells, line }));\n if (rows.length === 0) return [];\n const headers = options.headers ?? ((rows[0] as { cells: string[] }).cells as readonly string[]);\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(({ cells, line }) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const rows: string[][] = [];\n scan(text, options, (row) => rows.push(row));\n return rows;\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseRecordsWithLines(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = options.delimiter ?? ',';\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/** Serialize records to CSV with a header row; `columns` overrides column order. */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n","import { CsvScanner, detectDelimiter } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out — rows spanning chunk boundaries are assembled internally,\n * and the delimiter is auto-detected once the first line is available (unless\n * pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n let delimiter: string | undefined = options.delimiter;\n let scanner: CsvScanner | undefined =\n delimiter === undefined ? undefined : new CsvScanner(delimiter, options.skipEmptyLines !== false);\n let pending = '';\n const ensureScanner = (): CsvScanner => {\n if (scanner === undefined) {\n delimiter = detectDelimiter(pending);\n scanner = new CsvScanner(delimiter, options.skipEmptyLines !== false);\n }\n return scanner;\n };\n return new TransformStream<string, string[]>({\n flush(controller) {\n const active = ensureScanner();\n if (pending !== '') active.push(pending, (row) => controller.enqueue(row));\n active.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n if (scanner === undefined) {\n pending += chunk;\n // Wait for the first full line so auto-detection sees a complete header.\n if (!/[\\n\\r]/.test(pending)) return;\n ensureScanner().push(pending, (row) => controller.enqueue(row));\n pending = '';\n return;\n }\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n","import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseRecordsWithLines } from './csv';\nimport { TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseRecordsWithLines(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TypeError('tabula: async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tabula.js","names":["message","options","line","issues","errors","DEFAULT_NEWLINE","delimiter","firstLine","text","best","bestCount","candidate","count","quoted","i","char","onRow","scanner","rows","row","cells","headers","record","cell","index","header","value","raw","quoteAll","newline","lines","list","records","columns","column","controller","chunk","values","result","schema"],"sources":["../src/errors.ts","../src/csv.ts","../src/stream.ts","../src/typed.ts"],"sourcesContent":["/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */\nexport class TabulaError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = new.target.name;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */\nexport class TabulaParseError extends TabulaError {}\n\n/** One row failed schema validation; `line` is 1-based over physical document lines. */\nexport class TabulaRowError extends TabulaError {\n readonly issues: readonly unknown[];\n readonly line: number;\n\n constructor(line: number, issues: readonly unknown[]) {\n super(`row at line ${line} failed validation`, { cause: issues });\n this.line = line;\n this.issues = issues;\n }\n}\n\n/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */\nexport class TabulaValidationError extends TabulaError {\n readonly errors: readonly TabulaRowError[];\n\n constructor(errors: readonly TabulaRowError[], message = `${errors.length} invalid row(s)`) {\n super(message, { cause: errors });\n this.errors = errors;\n }\n}\n","import { TabulaParseError } from './errors';\nimport type {\n TabulaParseOptions,\n TabulaRecordOptions,\n TabulaRecordSerializeOptions,\n TabulaSerializeOptions,\n} from './types';\n\nconst DEFAULT_NEWLINE = '\\r\\n';\n\n/** A delimiter the scanner can match: exactly one character. */\nfunction assertSingleChar(delimiter: string): string {\n if (delimiter.length !== 1) {\n throw new TabulaParseError(`The delimiter must be a single character, got \"${delimiter}\".`);\n }\n return delimiter;\n}\n\n/** Detect the delimiter from the first line among comma, semicolon, and tab. */\nfunction detectDelimiter(text: string): string {\n const firstLine = text.slice(0, 2_048).split(/[\\n\\r]/, 1)[0] ?? '';\n let best = ',';\n let bestCount = -1;\n for (const candidate of ['\\t', ';', ',']) {\n let count = 0;\n let quoted = false;\n for (let i = 0; i < firstLine.length; i += 1) {\n const char = firstLine[i];\n if (char === '\"') quoted = !quoted;\n else if (char === candidate && !quoted) count += 1;\n }\n if (count > bestCount) {\n best = candidate;\n bestCount = count;\n }\n }\n return bestCount > 0 ? best : ',';\n}\n\n/**\n * Push-based RFC 4180 scanner: feed text, receive complete rows with the\n * 1-based physical line each started on. Rows may span chunks; `finish()`\n * flushes the pending row. Handles BOM, LF/CRLF/CR, quoted fields with\n * embedded delimiters, newlines and `\"\"` escapes.\n *\n * Without a pinned `delimiter` the scanner accumulates until the first line\n * completes, auto-detects, and starts emitting rows.\n */\nexport class CsvScanner {\n private delimiter: string | undefined;\n private readonly skipEmptyLines: boolean;\n private buffer = '';\n private field = '';\n private row: string[] = [];\n private inQuotes = false;\n private fieldWasQuoted = false;\n private pendingCr = false;\n private started = false;\n private line = 1;\n private rowLine = 1;\n\n constructor(options: TabulaParseOptions = {}) {\n this.delimiter = options.delimiter === undefined ? undefined : assertSingleChar(options.delimiter);\n this.skipEmptyLines = options.skipEmptyLines !== false;\n }\n\n /** Emit every completed row with the 1-based physical line it started on. */\n push(text: string, onRow: (row: string[], line: number) => void): void {\n if (this.delimiter === undefined) {\n this.buffer += text;\n // Auto-detection needs a complete first line; hold the chunks until then.\n if (!/[\\n\\r]/.test(this.buffer)) return;\n this.delimiter = detectDelimiter(this.buffer);\n text = this.buffer;\n this.buffer = '';\n }\n this.scan(text, onRow);\n }\n\n finish(onRow: (row: string[], line: number) => void): void {\n if (this.buffer !== '') {\n const text = this.buffer;\n this.buffer = '';\n this.delimiter = this.delimiter ?? detectDelimiter(text);\n this.scan(text, onRow);\n }\n if (this.field !== '' || this.row.length > 0 || this.fieldWasQuoted) this.endRow(onRow);\n this.pendingCr = false;\n }\n\n private scan(text: string, onRow: (row: string[], line: number) => void): void {\n const delimiter = this.delimiter as string;\n for (let i = 0; i < text.length; i += 1) {\n let char = text[i] as string;\n if (this.pendingCr) {\n this.pendingCr = false;\n if (char === '\\n') continue;\n }\n if (!this.started && i === 0 && char === '\\uFEFF') continue; // strip BOM\n this.started = true;\n if (char === '\\r') {\n this.pendingCr = true;\n char = '\\n';\n }\n if (this.inQuotes) {\n if (char === '\\n') this.line += 1; // newlines inside quotes are still physical lines\n if (char === '\"') {\n if (text[i + 1] === '\"') {\n this.field += '\"';\n i += 1;\n } else {\n this.inQuotes = false;\n }\n } else {\n this.field += char;\n }\n continue;\n }\n if (char === '\"' && !this.fieldWasQuoted && this.field === '') {\n this.inQuotes = true;\n this.fieldWasQuoted = true;\n continue;\n }\n if (char === '\"') {\n // A stray quote inside an unquoted field: keep it literally (forgiving mode).\n this.field += '\"';\n continue;\n }\n if (char === delimiter) {\n this.row.push(this.field);\n this.field = '';\n this.fieldWasQuoted = false;\n continue;\n }\n if (char === '\\n') {\n this.endRow(onRow);\n this.fieldWasQuoted = false;\n continue;\n }\n this.field += char;\n }\n }\n\n private endRow(onRow: (row: string[], line: number) => void): void {\n this.row.push(this.field);\n this.field = '';\n const isBlank = this.row.length === 1 && this.row[0] === '';\n if (!isBlank || !this.skipEmptyLines) onRow(this.row, this.rowLine);\n this.row = [];\n this.line += 1;\n this.rowLine = this.line;\n }\n}\n\n/**\n * Parse CSV/TSV text into a raw grid of string cells. Quoted fields may contain\n * the delimiter, newlines, and escaped quotes; blank lines are skipped by\n * default. This is the untyped core every other tabula reader builds on.\n */\nexport function parseGrid(text: string, options: TabulaParseOptions = {}): string[][] {\n const scanner = new CsvScanner(options);\n const rows: string[][] = [];\n scanner.push(text, (row) => rows.push(row));\n scanner.finish((row) => rows.push(row));\n return rows;\n}\n\n/** A row paired with the 1-based physical line it started on. */\ntype LineRow = { line: number; record: Record<string, string> };\n\n/**\n * Parse into records while keeping each row's physical start line: the shared\n * core of {@link parseRecords} and the typed reader. Headers default to the\n * first row; explicit `headers` make it headerless. Extra cells are dropped,\n * short rows leave keys off.\n */\nexport function parseTrackedRecords(text: string, options: TabulaRecordOptions = {}): LineRow[] {\n if (options.headers !== undefined && options.headers.length === 0) {\n throw new TabulaParseError('headers must name at least one column');\n }\n const scanner = new CsvScanner(options);\n const rows: [cells: string[], line: number][] = [];\n scanner.push(text, (cells, line) => rows.push([cells, line]));\n scanner.finish((cells, line) => rows.push([cells, line]));\n if (rows.length === 0) return [];\n const headers = options.headers ?? rows[0][0];\n const data = options.headers === undefined ? rows.slice(1) : rows;\n return data.map(([cells, line]) => {\n const record: Record<string, string> = {};\n headers.forEach((header, index) => {\n const cell = cells[index];\n if (cell !== undefined) record[header] = cell;\n });\n return { line, record };\n });\n}\n\n/**\n * Parse CSV text into `Record<string, string>` rows. By default the first row\n * supplies the headers; pass explicit `headers` to read a headerless document.\n * Rows with extra cells lose the overflow; short rows leave keys off the record.\n */\nexport function parseRecords(text: string, options: TabulaRecordOptions = {}): Record<string, string>[] {\n return parseTrackedRecords(text, options).map(({ record }) => record);\n}\n\nfunction needsQuotes(value: string, delimiter: string): boolean {\n return (\n value.includes(delimiter) ||\n value.includes('\"') ||\n value.includes('\\n') ||\n value.includes('\\r') ||\n value !== value.trim()\n );\n}\n\nfunction serializeCell(raw: unknown, delimiter: string, quoteAll: boolean): string {\n const value = raw === null || raw === undefined ? '' : String(raw);\n if (!quoteAll && !needsQuotes(value, delimiter)) return value;\n return `\"${value.replaceAll('\"', '\"\"')}\"`;\n}\n\n/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */\nexport function toCsv(rows: Iterable<readonly unknown[]>, options: TabulaSerializeOptions = {}): string {\n const delimiter = assertSingleChar(options.delimiter ?? ',');\n const newline = options.newline ?? DEFAULT_NEWLINE;\n const quoteAll = options.quoteAll === true;\n const lines: string[] = [];\n for (const row of rows) {\n lines.push(row.map((cell) => serializeCell(cell, delimiter, quoteAll)).join(delimiter));\n }\n return lines.join(newline) + (lines.length > 0 ? newline : '');\n}\n\n/**\n * Serialize records to CSV with a header row. Columns are the union of every\n * record's keys in first-seen order unless `columns` overrides the order.\n */\nexport function recordsToCsv(\n records: Iterable<Record<string, unknown>>,\n options: TabulaRecordSerializeOptions = {},\n): string {\n const list = [...records];\n const columns = options.columns ?? [...new Set(list.flatMap((record) => Object.keys(record)))];\n const rows: (readonly unknown[])[] = [columns];\n for (const record of list) rows.push(columns.map((column) => record[column] ?? ''));\n return toCsv(rows, options);\n}\n","import { CsvScanner } from './csv';\nimport type { TabulaParseOptions } from './types';\n\n/**\n * A `TransformStream<string, string[]>`: push raw text chunks in, complete\n * grid rows come out. The {@link CsvScanner} assembles rows across chunk\n * boundaries and auto-detects the delimiter once the first line is available\n * (unless pinned via `delimiter`). Pair with `TextDecoderStream` for files and\n * `@vielzeug/familiar` workers for off-main-thread parsing of large exports.\n */\nexport function createCsvGridStream(options: TabulaParseOptions = {}): TransformStream<string, string[]> {\n const scanner = new CsvScanner(options);\n return new TransformStream<string, string[]>({\n flush(controller) {\n scanner.finish((row) => controller.enqueue(row));\n },\n transform(chunk, controller) {\n scanner.push(chunk, (row) => controller.enqueue(row));\n },\n });\n}\n","import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseTrackedRecords } from './csv';\nimport { TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseTrackedRecords(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TabulaParseError('async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"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"}
|
package/dist/typed.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./csv.cjs"),t=require("./errors.cjs");function parseTypedRecords(n,r,i={}){let a=[],o=[];for(let{line:s,record:c}of e.
|
|
1
|
+
const e=require("./csv.cjs"),t=require("./errors.cjs");function parseTypedRecords(n,r,i={}){let a=[],o=[];for(let{line:s,record:c}of e.parseTrackedRecords(n,i)){let e=r[`~standard`].validate(c);if(e instanceof Promise)throw new t.TabulaParseError(`async schemas are not supported; use a sync schema`);e.issues===void 0?a.push(e.value):o.push(new t.TabulaRowError(s,e.issues))}if(o.length>0)throw new t.TabulaValidationError(o);return a}exports.parseTypedRecords=parseTypedRecords;
|
|
2
2
|
//# sourceMappingURL=typed.cjs.map
|
package/dist/typed.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typed.cjs","names":["
|
|
1
|
+
{"version":3,"file":"typed.cjs","names":["parseTrackedRecords","TabulaParseError","TabulaRowError","TabulaValidationError","require_csv","require_errors","values","errors","text","options","result","schema","record","line"],"sources":["../src/typed.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseTrackedRecords } from './csv';\nimport { TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseTrackedRecords(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TabulaParseError('async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":"A,uDAiBA,SAAgB,kBACd,EACA,EACA,EAA+B,CAAC,EAC3B,CACL,IAAMM,EAAc,CAAC,EACfC,EAA2B,CAAC,EAClC,IAAK,GAAM,CAAE,OAAM,YAAYP,EAAAA,oBAAoBQ,EAAMC,CAAO,EAAG,CACjE,IAAMC,EAASC,EAAO,YAAY,CAAC,SAASC,CAAM,EAClD,GAAIF,aAAkB,QACpB,MAAM,IAAIT,EAAAA,iBAAiB,oDAAoD,EAE7ES,EAAO,SAAW,IAAA,GACpBJ,EAAO,KAAKI,EAAO,KAAU,EAE7BH,EAAO,KAAK,IAAIL,EAAAA,eAAeW,EAAMH,EAAO,MAAM,CAAC,CAEvD,CACA,GAAIH,EAAO,OAAS,EAAG,MAAM,IAAIJ,EAAAA,sBAAsBI,CAAM,EAC7D,OAAOD,CACT,C"}
|
package/dist/typed.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { parseTrackedRecords as e } from "./csv.js";
|
|
2
|
+
import { TabulaParseError as t, TabulaRowError as n, TabulaValidationError as r } from "./errors.js";
|
|
3
3
|
// src/typed.ts
|
|
4
|
-
function parseTypedRecords(
|
|
5
|
-
let
|
|
6
|
-
for (let { line:
|
|
7
|
-
let e =
|
|
8
|
-
if (e instanceof Promise) throw
|
|
9
|
-
e.issues === void 0 ?
|
|
4
|
+
function parseTypedRecords(i, a, o = {}) {
|
|
5
|
+
let s = [], c = [];
|
|
6
|
+
for (let { line: r, record: l } of e(i, o)) {
|
|
7
|
+
let e = a["~standard"].validate(l);
|
|
8
|
+
if (e instanceof Promise) throw new t("async schemas are not supported; use a sync schema");
|
|
9
|
+
e.issues === void 0 ? s.push(e.value) : c.push(new n(r, e.issues));
|
|
10
10
|
}
|
|
11
|
-
if (
|
|
12
|
-
return
|
|
11
|
+
if (c.length > 0) throw new r(c);
|
|
12
|
+
return s;
|
|
13
13
|
}
|
|
14
14
|
export { parseTypedRecords };
|
|
15
15
|
|
package/dist/typed.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"typed.js","names":["values","errors","
|
|
1
|
+
{"version":3,"file":"typed.js","names":["values","errors","parseTrackedRecords","text","options","result","schema","record","TabulaParseError","TabulaRowError","line","TabulaValidationError"],"sources":["../src/typed.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@vielzeug/spell';\n\nimport { parseTrackedRecords } from './csv';\nimport { TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';\nimport type { TabulaRecordOptions } from './types';\n\n/**\n * Parse CSV text into typed rows validated by a Standard Schema (v1) — every\n * `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders\n * (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,\n * or dates in the same pass.\n *\n * All rows are validated before returning: on any failure a\n * {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based\n * `line` + schema issues) per invalid row, so a bad export is reported in full\n * instead of at the first bad cell.\n */\nexport function parseTypedRecords<T>(\n text: string,\n schema: StandardSchemaV1<Record<string, unknown>, T>,\n options: TabulaRecordOptions = {},\n): T[] {\n const values: T[] = [];\n const errors: TabulaRowError[] = [];\n for (const { line, record } of parseTrackedRecords(text, options)) {\n const result = schema['~standard'].validate(record);\n if (result instanceof Promise) {\n throw new TabulaParseError('async schemas are not supported; use a sync schema');\n }\n if (result.issues === undefined) {\n values.push(result.value as T);\n } else {\n errors.push(new TabulaRowError(line, result.issues));\n }\n }\n if (errors.length > 0) throw new TabulaValidationError(errors);\n return values;\n}\n"],"mappings":";;;AAiBA,SAAgB,kBACd,GACA,GACA,IAA+B,CAAC,GAC3B;CACL,IAAMA,IAAc,CAAC,GACfC,IAA2B,CAAC;CAClC,KAAK,IAAM,EAAE,SAAM,eAAYC,EAAoBC,GAAMC,CAAO,GAAG;EACjE,IAAMC,IAASC,EAAO,YAAY,CAAC,SAASC,CAAM;EAClD,IAAIF,aAAkB,SACpB,MAAM,IAAIG,EAAiB,oDAAoD;EAEjF,AAAIH,EAAO,WAAW,KAAA,IACpBL,EAAO,KAAKK,EAAO,KAAU,IAE7BJ,EAAO,KAAK,IAAIQ,EAAeC,GAAML,EAAO,MAAM,CAAC;CAEvD;CACA,IAAIJ,EAAO,SAAS,GAAG,MAAM,IAAIU,EAAsBV,CAAM;CAC7D,OAAOD;AACT"}
|
package/dist/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export type TabulaSerializeOptions = TabulaOptions & {
|
|
|
20
20
|
/** Line terminator (default RFC 4180 `\r\n`). */
|
|
21
21
|
newline?: string;
|
|
22
22
|
};
|
|
23
|
-
/** Options for record serialization;
|
|
23
|
+
/** Options for record serialization; columns default to every record's keys in first-seen order. */
|
|
24
24
|
export type TabulaRecordSerializeOptions = TabulaSerializeOptions & {
|
|
25
25
|
/** Explicit column order. */
|
|
26
26
|
columns?: readonly string[];
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG;IAC1B,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG;IAC/C,iFAAiF;IACjF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,GAAG;IACrD,gFAAgF;IAChF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B,CAAC;AAEF,0BAA0B;AAC1B,MAAM,MAAM,sBAAsB,GAAG,aAAa,GAAG;IACnD,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG;IAC1B,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,sDAAsD;AACtD,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG;IAC/C,iFAAiF;IACjF,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,mBAAmB,GAAG,kBAAkB,GAAG;IACrD,gFAAgF;IAChF,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B,CAAC;AAEF,0BAA0B;AAC1B,MAAM,MAAM,sBAAsB,GAAG,aAAa,GAAG;IACnD,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,oGAAoG;AACpG,MAAM,MAAM,4BAA4B,GAAG,sBAAsB,GAAG;IAClE,6BAA6B;IAC7B,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/tabula",
|
|
3
|
-
"version": "26.11.
|
|
3
|
+
"version": "26.11.15",
|
|
4
4
|
"description": "Typed CSV and TSV: RFC 4180 parser with schema-validated rows, streaming reader, and safe serializer.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"test": "vitest"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@vielzeug/spell": "26.11.
|
|
43
|
+
"@vielzeug/spell": "26.11.15"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^26.6.6",
|