@vielzeug/tabula 0.0.0-stage → 26.11.13
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/LICENSE +21 -0
- package/README.md +40 -2
- package/dist/csv.cjs +8 -0
- package/dist/csv.cjs.map +1 -0
- package/dist/csv.d.ts +44 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/csv.js +96 -0
- package/dist/csv.js.map +1 -0
- package/dist/errors.cjs +2 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +20 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/stream.cjs +2 -0
- package/dist/stream.cjs.map +1 -0
- package/dist/stream.d.ts +10 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +20 -0
- package/dist/stream.js.map +1 -0
- package/dist/tabula.cjs +8 -0
- package/dist/tabula.cjs.map +1 -0
- package/dist/tabula.iife.js +8 -0
- package/dist/tabula.iife.js.map +1 -0
- package/dist/tabula.js +137 -0
- package/dist/tabula.js.map +1 -0
- package/dist/typed.cjs +2 -0
- package/dist/typed.cjs.map +1 -0
- package/dist/typed.d.ts +15 -0
- package/dist/typed.d.ts.map +1 -0
- package/dist/typed.js +16 -0
- package/dist/typed.js.map +1 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +49 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Helmuth Saatkamp
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @vielzeug/tabula
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Typed CSV and TSV: RFC 4180 parser with schema-validated rows, streaming reader, and safe serializer.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @vielzeug/tabula
|
|
9
|
+
npm install @vielzeug/tabula
|
|
10
|
+
yarn add @vielzeug/tabula
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { parseRecords, recordsToCsv } from '@vielzeug/tabula';
|
|
17
|
+
|
|
18
|
+
const rows = parseRecords('id,total\n7,"1,200.00"\n8,99.50\n');
|
|
19
|
+
// [{ id: '7', total: '1,200.00' }, { id: '8', total: '99.50' }]
|
|
20
|
+
|
|
21
|
+
console.log(recordsToCsv([{ a: 1, b: 'x,y' }]));
|
|
22
|
+
// a,b
|
|
23
|
+
// 1,"x,y"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
No third-party runtime dependencies. One push-based scanner backs the string parser, the
|
|
27
|
+
`TransformStream` reader, and the schema-typed pipeline, so paste boxes, uploads, and
|
|
28
|
+
gigabyte exports parse with identical semantics. `parseTypedRecords` hands rows to any
|
|
29
|
+
Spell/standard-schema validator and aggregates every failure with its line number.
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
- [Overview](https://vielzeug.dev/tabula/)
|
|
34
|
+
- [Usage Guide](https://vielzeug.dev/tabula/usage)
|
|
35
|
+
- [API Reference](https://vielzeug.dev/tabula/api)
|
|
36
|
+
- [Examples](https://vielzeug.dev/tabula/examples)
|
|
37
|
+
- [Migration Guide](https://vielzeug.dev/tabula/migration)
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
MIT © [Helmuth Saatkamp](https://github.com/helmuthdu): part of the [Vielzeug](https://github.com/helmuthdu/vielzeug) monorepo.
|
package/dist/csv.cjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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=!0){this.delimiter=e,this.skipEmptyLines=t}push(e,t){for(let n=0;n<e.length;n+=1){let r=e[n];this.pendingCr&&(this.pendingCr=!1,r===`
|
|
3
|
+
`)||(this.started||n!==0||r!==``)&&(this.started=!0,r===`\r`&&(this.pendingCr=!0,r=`
|
|
4
|
+
`),this.inQuotes?(r===`
|
|
5
|
+
`&&(this.line+=1),r===`"`?e[n+1]===`"`?(this.field+=`"`,n+=1):this.inQuotes=!1:this.field+=r):r===`"`&&!this.fieldWasQuoted&&this.field===``?(this.inQuotes=!0,this.fieldWasQuoted=!0):r===`"`?this.field+=`"`:r===this.delimiter?(this.row.push(this.field),this.field=``,this.fieldWasQuoted=!1):r===`
|
|
6
|
+
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=r)}}finish(e){(this.field!==``||this.row.length>0||this.fieldWasQuoted)&&this.endRow(e),this.pendingCr=!1}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 scan(e,t,n){let r=new CsvScanner(t.delimiter??detectDelimiter(e),t.skipEmptyLines!==!1);r.push(e,n),r.finish(n)}function parseRecordsWithLines(t,n={}){if(n.headers!==void 0&&n.headers.length===0)throw new e.TabulaParseError(`headers must name at least one column`);let r=[];if(scan(t,n,(e,t)=>r.push({cells:e,line:t})),r.length===0)return[];let i=n.headers??r[0].cells;return(n.headers===void 0?r.slice(1):r).map(({cells:e,line:t})=>{let n={};return i.forEach((t,r)=>{let i=e[r];i!==void 0&&(n[t]=i)}),{line:t,record:n}})}function parseGrid(e,t={}){let n=[];return scan(e,t,e=>n.push(e)),n}function parseRecords(e,t={}){return parseRecordsWithLines(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=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.detectDelimiter=detectDelimiter,exports.parseGrid=parseGrid,exports.parseRecords=parseRecords,exports.parseRecordsWithLines=parseRecordsWithLines,exports.recordsToCsv=recordsToCsv,exports.toCsv=toCsv;
|
|
8
|
+
//# sourceMappingURL=csv.cjs.map
|
package/dist/csv.cjs.map
ADDED
|
@@ -0,0 +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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"}
|
package/dist/csv.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
/**
|
|
5
|
+
* Push-based RFC 4180 scanner: feed text, receive complete rows. Rows may span
|
|
6
|
+
* chunks; `finish()` flushes the pending row. Handles BOM, LF/CRLF/CR, quoted
|
|
7
|
+
* fields with embedded delimiters, newlines and `""` escapes.
|
|
8
|
+
*/
|
|
9
|
+
export declare class CsvScanner {
|
|
10
|
+
private readonly delimiter;
|
|
11
|
+
private readonly skipEmptyLines;
|
|
12
|
+
private field;
|
|
13
|
+
private row;
|
|
14
|
+
private inQuotes;
|
|
15
|
+
private fieldWasQuoted;
|
|
16
|
+
private pendingCr;
|
|
17
|
+
private started;
|
|
18
|
+
private line;
|
|
19
|
+
private rowLine;
|
|
20
|
+
constructor(delimiter: string, skipEmptyLines?: boolean);
|
|
21
|
+
/** Emit every completed row with the 1-based physical line it started on. */
|
|
22
|
+
push(text: string, onRow: (row: string[], line: number) => void): void;
|
|
23
|
+
finish(onRow: (row: string[], line: number) => void): void;
|
|
24
|
+
private endRow;
|
|
25
|
+
}
|
|
26
|
+
/** A row paired with the 1-based physical line it started on. */
|
|
27
|
+
export type LineRow = {
|
|
28
|
+
line: number;
|
|
29
|
+
record: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
/** Parse into records while keeping each row's physical start line (for error reporting). */
|
|
32
|
+
export declare function parseRecordsWithLines(text: string, options?: TabulaRecordOptions): LineRow[];
|
|
33
|
+
export declare function parseGrid(text: string, options?: TabulaParseOptions): string[][];
|
|
34
|
+
/**
|
|
35
|
+
* Parse CSV text into `Record<string, string>` rows. By default the first row
|
|
36
|
+
* supplies the headers; pass explicit `headers` to read a headerless document.
|
|
37
|
+
* Rows with extra cells lose the overflow; short rows leave keys off the record.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseRecords(text: string, options?: TabulaRecordOptions): Record<string, string>[];
|
|
40
|
+
/** Serialize a grid of values to CSV text (RFC 4180 quoting, CRLF lines by default). */
|
|
41
|
+
export declare function toCsv(rows: Iterable<readonly unknown[]>, options?: TabulaSerializeOptions): string;
|
|
42
|
+
/** Serialize records to CSV with a header row; `columns` overrides column order. */
|
|
43
|
+
export declare function recordsToCsv(records: Iterable<Record<string, unknown>>, options?: TabulaRecordSerializeOptions): string;
|
|
44
|
+
//# sourceMappingURL=csv.d.ts.map
|
|
@@ -0,0 +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;AAIjB,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBpD;AAED;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,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,SAAS,EAAE,MAAM,EAAE,cAAc,UAAO;IAKpD,6EAA6E;IAC7E,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAoDtE,MAAM,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAK1D,OAAO,CAAC,MAAM;CASf;AAcD,iEAAiE;AACjE,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAAC;AAEvE,6FAA6F;AAC7F,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,EAAE,CAiBhG;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,EAAE,EAAE,CAIpF;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,oFAAoF;AACpF,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
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { TabulaParseError as e } from "./errors.js";
|
|
2
|
+
// src/csv.ts
|
|
3
|
+
var t = "\r\n";
|
|
4
|
+
function detectDelimiter(e) {
|
|
5
|
+
let t = e.slice(0, 2048).split(/[\n\r]/, 1)[0] ?? "", n = ",", r = -1;
|
|
6
|
+
for (let e of [
|
|
7
|
+
" ",
|
|
8
|
+
";",
|
|
9
|
+
","
|
|
10
|
+
]) {
|
|
11
|
+
let i = 0, a = !1;
|
|
12
|
+
for (let n = 0; n < t.length; n += 1) {
|
|
13
|
+
let r = t[n];
|
|
14
|
+
r === "\"" ? a = !a : r === e && !a && (i += 1);
|
|
15
|
+
}
|
|
16
|
+
i > r && (n = e, r = i);
|
|
17
|
+
}
|
|
18
|
+
return r > 0 ? n : ",";
|
|
19
|
+
}
|
|
20
|
+
var CsvScanner = class {
|
|
21
|
+
delimiter;
|
|
22
|
+
skipEmptyLines;
|
|
23
|
+
field = "";
|
|
24
|
+
row = [];
|
|
25
|
+
inQuotes = !1;
|
|
26
|
+
fieldWasQuoted = !1;
|
|
27
|
+
pendingCr = !1;
|
|
28
|
+
started = !1;
|
|
29
|
+
line = 1;
|
|
30
|
+
rowLine = 1;
|
|
31
|
+
constructor(e, t = !0) {
|
|
32
|
+
this.delimiter = e, this.skipEmptyLines = t;
|
|
33
|
+
}
|
|
34
|
+
push(e, t) {
|
|
35
|
+
for (let n = 0; n < e.length; n += 1) {
|
|
36
|
+
let r = e[n];
|
|
37
|
+
this.pendingCr && (this.pendingCr = !1, r === "\n") || (this.started || n !== 0 || r !== "") && (this.started = !0, r === "\r" && (this.pendingCr = !0, r = "\n"), this.inQuotes ? (r === "\n" && (this.line += 1), r === "\"" ? e[n + 1] === "\"" ? (this.field += "\"", n += 1) : this.inQuotes = !1 : this.field += r) : r === "\"" && !this.fieldWasQuoted && this.field === "" ? (this.inQuotes = !0, this.fieldWasQuoted = !0) : r === "\"" ? this.field += "\"" : r === this.delimiter ? (this.row.push(this.field), this.field = "", this.fieldWasQuoted = !1) : r === "\n" ? (this.endRow(t), this.fieldWasQuoted = !1) : this.field += r);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finish(e) {
|
|
41
|
+
(this.field !== "" || this.row.length > 0 || this.fieldWasQuoted) && this.endRow(e), this.pendingCr = !1;
|
|
42
|
+
}
|
|
43
|
+
endRow(e) {
|
|
44
|
+
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
|
+
}
|
|
46
|
+
};
|
|
47
|
+
function scan(e, t, n) {
|
|
48
|
+
let r = new CsvScanner(t.delimiter ?? detectDelimiter(e), t.skipEmptyLines !== !1);
|
|
49
|
+
r.push(e, n), r.finish(n);
|
|
50
|
+
}
|
|
51
|
+
function parseRecordsWithLines(t, n = {}) {
|
|
52
|
+
if (n.headers !== void 0 && n.headers.length === 0) throw new e("headers must name at least one column");
|
|
53
|
+
let r = [];
|
|
54
|
+
if (scan(t, n, (e, t) => r.push({
|
|
55
|
+
cells: e,
|
|
56
|
+
line: t
|
|
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 }) => {
|
|
60
|
+
let n = {};
|
|
61
|
+
return i.forEach((t, r) => {
|
|
62
|
+
let i = e[r];
|
|
63
|
+
i !== void 0 && (n[t] = i);
|
|
64
|
+
}), {
|
|
65
|
+
line: t,
|
|
66
|
+
record: n
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function parseGrid(e, t = {}) {
|
|
71
|
+
let n = [];
|
|
72
|
+
return scan(e, t, (e) => n.push(e)), n;
|
|
73
|
+
}
|
|
74
|
+
function parseRecords(e, t = {}) {
|
|
75
|
+
return parseRecordsWithLines(e, t).map(({ record: e }) => e);
|
|
76
|
+
}
|
|
77
|
+
function needsQuotes(e, t) {
|
|
78
|
+
return e.includes(t) || e.includes("\"") || e.includes("\n") || e.includes("\r") || e !== e.trim();
|
|
79
|
+
}
|
|
80
|
+
function serializeCell(e, t, n) {
|
|
81
|
+
let r = e == null ? "" : String(e);
|
|
82
|
+
return !n && !needsQuotes(r, t) ? r : `"${r.replaceAll("\"", "\"\"")}"`;
|
|
83
|
+
}
|
|
84
|
+
function toCsv(e, n = {}) {
|
|
85
|
+
let r = n.delimiter ?? ",", i = n.newline ?? t, a = n.quoteAll === !0, o = [];
|
|
86
|
+
for (let t of e) o.push(t.map((e) => serializeCell(e, r, a)).join(r));
|
|
87
|
+
return o.join(i) + (o.length > 0 ? i : "");
|
|
88
|
+
}
|
|
89
|
+
function recordsToCsv(e, t = {}) {
|
|
90
|
+
let n = [...e], r = t.columns ?? [...new Set(n.flatMap((e) => Object.keys(e)))], i = [r];
|
|
91
|
+
for (let e of n) i.push(r.map((t) => e[t] ?? ""));
|
|
92
|
+
return toCsv(i, t);
|
|
93
|
+
}
|
|
94
|
+
export { CsvScanner, detectDelimiter, parseGrid, parseRecords, parseRecordsWithLines, recordsToCsv, toCsv };
|
|
95
|
+
|
|
96
|
+
//# sourceMappingURL=csv.js.map
|
package/dist/csv.js.map
ADDED
|
@@ -0,0 +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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"}
|
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
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}};exports.TabulaError=TabulaError,exports.TabulaParseError=TabulaParseError,exports.TabulaRowError=TabulaRowError,exports.TabulaValidationError=TabulaValidationError;
|
|
2
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.cjs","names":["message","options","line","issues","errors"],"sources":["../src/errors.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"],"mappings":"AACA,IAAa,YAAb,cAAiC,KAAM,CACrC,YAAY,EAAiB,EAAwB,CACnD,MAAMA,EAASC,CAAO,EACtB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,iBAAb,cAAsC,WAAY,CAAC,EAGtC,eAAb,cAAoC,WAAY,CAC9C,OACA,KAEA,YAAY,EAAc,EAA4B,CACpD,MAAM,eAAeC,EAAK,oBAAqB,CAAE,MAAOC,CAAO,CAAC,EAChE,KAAK,KAAOD,EACZ,KAAK,OAASC,CAChB,CACF,EAGa,sBAAb,cAA2C,WAAY,CACrD,OAEA,YAAY,EAAmC,EAAU,GAAGC,EAAO,OAAO,iBAAkB,CAC1F,MAAMJ,EAAS,CAAE,MAAOI,CAAO,CAAC,EAChC,KAAK,OAASA,CAChB,CACF,E"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Base class for all tabula errors. Use `instanceof TabulaError` to catch any tabula-originated error. */
|
|
2
|
+
export declare class TabulaError extends Error {
|
|
3
|
+
constructor(message: string, options?: ErrorOptions);
|
|
4
|
+
}
|
|
5
|
+
/** A CSV document that cannot be read at all (e.g. an empty header request with no rows). */
|
|
6
|
+
export declare class TabulaParseError extends TabulaError {
|
|
7
|
+
}
|
|
8
|
+
/** One row failed schema validation; `line` is 1-based over physical document lines. */
|
|
9
|
+
export declare class TabulaRowError extends TabulaError {
|
|
10
|
+
readonly issues: readonly unknown[];
|
|
11
|
+
readonly line: number;
|
|
12
|
+
constructor(line: number, issues: readonly unknown[]);
|
|
13
|
+
}
|
|
14
|
+
/** Aggregate of every invalid row from a typed parse; inspect `errors` per line. */
|
|
15
|
+
export declare class TabulaValidationError extends TabulaError {
|
|
16
|
+
readonly errors: readonly TabulaRowError[];
|
|
17
|
+
constructor(errors: readonly TabulaRowError[], message?: string);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,2GAA2G;AAC3G,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY;CAKpD;AAED,6FAA6F;AAC7F,qBAAa,gBAAiB,SAAQ,WAAW;CAAG;AAEpD,wFAAwF;AACxF,qBAAa,cAAe,SAAQ,WAAW;IAC7C,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE;CAKrD;AAED,oFAAoF;AACpF,qBAAa,qBAAsB,SAAQ,WAAW;IACpD,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;gBAE/B,MAAM,EAAE,SAAS,cAAc,EAAE,EAAE,OAAO,SAAoC;CAI3F"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var TabulaError = class extends Error {
|
|
3
|
+
constructor(e, t) {
|
|
4
|
+
super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
|
|
5
|
+
}
|
|
6
|
+
}, TabulaParseError = class extends TabulaError {}, TabulaRowError = class extends TabulaError {
|
|
7
|
+
issues;
|
|
8
|
+
line;
|
|
9
|
+
constructor(e, t) {
|
|
10
|
+
super(`row at line ${e} failed validation`, { cause: t }), this.line = e, this.issues = t;
|
|
11
|
+
}
|
|
12
|
+
}, TabulaValidationError = class extends TabulaError {
|
|
13
|
+
errors;
|
|
14
|
+
constructor(e, t = `${e.length} invalid row(s)`) {
|
|
15
|
+
super(t, { cause: e }), this.errors = e;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
export { TabulaError, TabulaParseError, TabulaRowError, TabulaValidationError };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":["message","options","line","issues","errors"],"sources":["../src/errors.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"],"mappings":";AACA,IAAa,cAAb,cAAiC,MAAM;CACrC,YAAY,GAAiB,GAAwB;EAGnD,AAFA,MAAMA,GAASC,CAAO,GACtB,KAAK,OAAO,WAAW,MACvB,OAAO,eAAe,MAAM,WAAW,SAAS;CAClD;AACF,GAGa,mBAAb,cAAsC,YAAY,CAAC,GAGtC,iBAAb,cAAoC,YAAY;CAC9C;CACA;CAEA,YAAY,GAAc,GAA4B;EAGpD,AAFA,MAAM,eAAeC,EAAK,qBAAqB,EAAE,OAAOC,EAAO,CAAC,GAChE,KAAK,OAAOD,GACZ,KAAK,SAASC;CAChB;AACF,GAGa,wBAAb,cAA2C,YAAY;CACrD;CAEA,YAAY,GAAmC,IAAU,GAAGC,EAAO,OAAO,kBAAkB;EAE1F,AADA,MAAMJ,GAAS,EAAE,OAAOI,EAAO,CAAC,GAChC,KAAK,SAASA;CAChB;AACF"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +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.detectDelimiter=e.detectDelimiter,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
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { CsvScanner, detectDelimiter, parseGrid, parseRecords, recordsToCsv, toCsv } from './csv';
|
|
2
|
+
export { TabulaError, TabulaParseError, TabulaRowError, TabulaValidationError } from './errors';
|
|
3
|
+
export { createCsvGridStream } from './stream';
|
|
4
|
+
export { parseTypedRecords } from './typed';
|
|
5
|
+
export type { TabulaOptions, TabulaParseOptions, TabulaRecordOptions, TabulaRecordSerializeOptions, TabulaSerializeOptions, } from './types';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAClG,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
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { CsvScanner as e, detectDelimiter as t, parseGrid as n, parseRecords as r, recordsToCsv as i, toCsv as a } from "./csv.js";
|
|
2
|
+
import { TabulaError as o, TabulaParseError as s, TabulaRowError as c, TabulaValidationError as l } from "./errors.js";
|
|
3
|
+
import { createCsvGridStream as u } from "./stream.js";
|
|
4
|
+
import { parseTypedRecords as d } from "./typed.js";
|
|
5
|
+
export { e as CsvScanner, o as TabulaError, s as TabulaParseError, c as TabulaRowError, l as TabulaValidationError, u as createCsvGridStream, t as detectDelimiter, n as parseGrid, r as parseRecords, d as parseTypedRecords, i as recordsToCsv, a as toCsv };
|
package/dist/stream.cjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=require("./csv.cjs");function createCsvGridStream(t={}){let n=t.delimiter,r=n===void 0?void 0:new e.CsvScanner(n,t.skipEmptyLines!==!1),i=``,ensureScanner=()=>(r===void 0&&(n=e.detectDelimiter(i),r=new e.CsvScanner(n,t.skipEmptyLines!==!1)),r);return new TransformStream({flush(e){let t=ensureScanner();i!==``&&t.push(i,t=>e.enqueue(t)),t.finish(t=>e.enqueue(t))},transform(e,t){if(r===void 0){if(i+=e,!/[\n\r]/.test(i))return;ensureScanner().push(i,e=>t.enqueue(e)),i=``}else r.push(e,e=>t.enqueue(e))}})}exports.createCsvGridStream=createCsvGridStream;
|
|
2
|
+
//# sourceMappingURL=stream.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.cjs","names":["CsvScanner","detectDelimiter","require_csv","delimiter","options","scanner","pending","active","controller","row","chunk"],"sources":["../src/stream.ts"],"sourcesContent":["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"],"mappings":"A,6BAUA,SAAgB,oBAAoB,EAA8B,CAAC,EAAsC,CACvG,IAAIG,EAAgCC,EAAQ,UACxCC,EACFF,IAAc,IAAA,GAAY,IAAA,GAAY,IAAIH,EAAAA,WAAWG,EAAWC,EAAQ,iBAAmB,EAAK,EAC9FE,EAAU,GACR,mBACAD,IAAY,IAAA,KACd,EAAYJ,EAAAA,gBAAgBK,CAAO,EACnC,EAAU,IAAIN,EAAAA,WAAWG,EAAWC,EAAQ,iBAAmB,EAAK,GAE/DC,GAET,OAAO,IAAI,gBAAkC,CAC3C,MAAM,EAAY,CAChB,IAAME,EAAS,cAAc,EACzBD,IAAY,IAAIC,EAAO,KAAKD,EAAU,GAAQE,EAAW,QAAQC,CAAG,CAAC,EACzEF,EAAO,OAAQ,GAAQC,EAAW,QAAQC,CAAG,CAAC,CAChD,EACA,UAAU,EAAO,EAAY,CAC3B,GAAIJ,IAAY,IAAA,GAAW,CAGzB,GAFA,GAAWK,EAEP,CAAC,SAAS,KAAKJ,CAAO,EAAG,OAC7B,cAAc,CAAC,CAAC,KAAKA,EAAU,GAAQE,EAAW,QAAQC,CAAG,CAAC,EAC9D,EAAU,EAEZ,MACAJ,EAAQ,KAAKK,EAAQ,GAAQF,EAAW,QAAQC,CAAG,CAAC,CACtD,CACF,CAAC,CACH,C"}
|
package/dist/stream.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TabulaParseOptions } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* A `TransformStream<string, string[]>`: push raw text chunks in, complete
|
|
4
|
+
* grid rows come out — rows spanning chunk boundaries are assembled internally,
|
|
5
|
+
* and the delimiter is auto-detected once the first line is available (unless
|
|
6
|
+
* pinned via `delimiter`). Pair with `TextDecoderStream` for files and
|
|
7
|
+
* `@vielzeug/familiar` workers for off-main-thread parsing of large exports.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createCsvGridStream(options?: TabulaParseOptions): TransformStream<string, string[]>;
|
|
10
|
+
//# sourceMappingURL=stream.d.ts.map
|
|
@@ -0,0 +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,CA8BvG"}
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CsvScanner as e, detectDelimiter as t } from "./csv.js";
|
|
2
|
+
// src/stream.ts
|
|
3
|
+
function createCsvGridStream(n = {}) {
|
|
4
|
+
let r = n.delimiter, i = r === void 0 ? void 0 : new e(r, n.skipEmptyLines !== !1), a = "", ensureScanner = () => (i === void 0 && (r = t(a), i = new e(r, n.skipEmptyLines !== !1)), i);
|
|
5
|
+
return new TransformStream({
|
|
6
|
+
flush(e) {
|
|
7
|
+
let t = ensureScanner();
|
|
8
|
+
a !== "" && t.push(a, (t) => e.enqueue(t)), t.finish((t) => e.enqueue(t));
|
|
9
|
+
},
|
|
10
|
+
transform(e, t) {
|
|
11
|
+
if (i === void 0) {
|
|
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));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export { createCsvGridStream };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=stream.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.js","names":["delimiter","options","scanner","CsvScanner","pending","detectDelimiter","active","controller","row","chunk"],"sources":["../src/stream.ts"],"sourcesContent":["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"],"mappings":";;AAUA,SAAgB,oBAAoB,IAA8B,CAAC,GAAsC;CACvG,IAAIA,IAAgCC,EAAQ,WACxCC,IACFF,MAAc,KAAA,IAAY,KAAA,IAAY,IAAIG,EAAWH,GAAWC,EAAQ,mBAAmB,EAAK,GAC9FG,IAAU,IACR,uBACAF,MAAY,KAAA,MACd,IAAYG,EAAgBD,CAAO,GACnC,IAAU,IAAID,EAAWH,GAAWC,EAAQ,mBAAmB,EAAK,IAE/DC;CAET,OAAO,IAAI,gBAAkC;EAC3C,MAAM,GAAY;GAChB,IAAMI,IAAS,cAAc;GAE7B,AADIF,MAAY,MAAIE,EAAO,KAAKF,IAAU,MAAQG,EAAW,QAAQC,CAAG,CAAC,GACzEF,EAAO,QAAQ,MAAQC,EAAW,QAAQC,CAAG,CAAC;EAChD;EACA,UAAU,GAAO,GAAY;GAC3B,IAAIN,MAAY,KAAA,GAAW;IAGzB,IAFA,KAAWO,GAEP,CAAC,SAAS,KAAKL,CAAO,GAAG;IAE7B,AADA,cAAc,CAAC,CAAC,KAAKA,IAAU,MAAQG,EAAW,QAAQC,CAAG,CAAC,GAC9D,IAAU;GAEZ,OACA,EAAQ,KAAKC,IAAQ,MAAQF,EAAW,QAAQC,CAAG,CAAC;EACtD;CACF,CAAC;AACH"}
|
package/dist/tabula.cjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
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=!0){this.delimiter=e,this.skipEmptyLines=t}push(e,t){for(let n=0;n<e.length;n+=1){let r=e[n];this.pendingCr&&(this.pendingCr=!1,r===`
|
|
3
|
+
`)||(this.started||n!==0||r!==``)&&(this.started=!0,r===`\r`&&(this.pendingCr=!0,r=`
|
|
4
|
+
`),this.inQuotes?(r===`
|
|
5
|
+
`&&(this.line+=1),r===`"`?e[n+1]===`"`?(this.field+=`"`,n+=1):this.inQuotes=!1:this.field+=r):r===`"`&&!this.fieldWasQuoted&&this.field===``?(this.inQuotes=!0,this.fieldWasQuoted=!0):r===`"`?this.field+=`"`:r===this.delimiter?(this.row.push(this.field),this.field=``,this.fieldWasQuoted=!1):r===`
|
|
6
|
+
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=r)}}finish(e){(this.field!==``||this.row.length>0||this.fieldWasQuoted)&&this.endRow(e),this.pendingCr=!1}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 scan(e,t,n){let r=new CsvScanner(t.delimiter??detectDelimiter(e),t.skipEmptyLines!==!1);r.push(e,n),r.finish(n)}function parseRecordsWithLines(e,t={}){if(t.headers!==void 0&&t.headers.length===0)throw new TabulaParseError(`headers must name at least one column`);let n=[];if(scan(e,t,(e,t)=>n.push({cells:e,line:t})),n.length===0)return[];let r=t.headers??n[0].cells;return(t.headers===void 0?n.slice(1):n).map(({cells:e,line:t})=>{let n={};return r.forEach((t,r)=>{let i=e[r];i!==void 0&&(n[t]=i)}),{line:t,record:n}})}function parseGrid(e,t={}){let n=[];return scan(e,t,e=>n.push(e)),n}function parseRecords(e,t={}){return parseRecordsWithLines(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=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=e.delimiter,n=t===void 0?void 0:new CsvScanner(t,e.skipEmptyLines!==!1),r=``,ensureScanner=()=>(n===void 0&&(t=detectDelimiter(r),n=new CsvScanner(t,e.skipEmptyLines!==!1)),n);return new TransformStream({flush(e){let t=ensureScanner();r!==``&&t.push(r,t=>e.enqueue(t)),t.finish(t=>e.enqueue(t))},transform(e,t){if(n===void 0){if(r+=e,!/[\n\r]/.test(r))return;ensureScanner().push(r,e=>t.enqueue(e)),r=``}else n.push(e,e=>t.enqueue(e))}})}function parseTypedRecords(e,t,n={}){let r=[],i=[];for(let{line:a,record:o}of parseRecordsWithLines(e,n)){let e=t[`~standard`].validate(o);if(e instanceof Promise)throw TypeError(`tabula: 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.detectDelimiter=detectDelimiter,exports.parseGrid=parseGrid,exports.parseRecords=parseRecords,exports.parseTypedRecords=parseTypedRecords,exports.recordsToCsv=recordsToCsv,exports.toCsv=toCsv;
|
|
8
|
+
//# sourceMappingURL=tabula.cjs.map
|
|
@@ -0,0 +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":"A,mEACA,IAAa,YAAb,cAAiC,KAAM,CACrC,YAAY,EAAiB,EAAwB,CACnD,MAAMA,EAASC,CAAO,EACtB,KAAK,KAAO,WAAW,KACvB,OAAO,eAAe,KAAM,WAAW,SAAS,CAClD,CACF,EAGa,iBAAb,cAAsC,WAAY,CAAC,EAGtC,eAAb,cAAoC,WAAY,CAC9C,OACA,KAEA,YAAY,EAAc,EAA4B,CACpD,MAAM,eAAeC,EAAK,oBAAqB,CAAE,MAAOC,CAAO,CAAC,EAChE,KAAK,KAAOD,EACZ,KAAK,OAASC,CAChB,CACF,EAGa,sBAAb,cAA2C,WAAY,CACrD,OAEA,YAAY,EAAmC,EAAU,GAAGC,EAAO,OAAO,iBAAkB,CAC1F,MAAMJ,EAAS,CAAE,MAAOI,CAAO,CAAC,EAChC,KAAK,OAASA,CAChB,CACF,ECxBMC,EAAkB;EAGxB,SAAgB,gBAAgB,EAAsB,CACpD,IAAMC,EAAYC,EAAK,MAAM,EAAG,IAAK,CAAC,CAAC,MAAM,SAAU,CAAC,CAAC,CAAC,IAAM,GAC5DC,EAAO,IACPC,EAAY,GAChB,IAAK,IAAMC,IAAa,CAAC,IAAM,IAAK,GAAG,EAAG,CACxC,IAAIC,EAAQ,EACRC,EAAS,GACb,IAAK,IAAIC,EAAI,EAAGA,EAAIP,EAAU,OAAQ,GAAK,EAAG,CAC5C,IAAMQ,EAAOR,EAAUO,GACnBC,IAAS,IAAK,EAAS,CAACF,EACnBE,IAASJ,GAAa,CAACE,IAAQ,GAAS,EACnD,CACID,EAAQF,IACV,EAAOC,EACP,EAAYC,EAEhB,CACA,OAAOF,EAAY,EAAID,EAAO,GAChC,CAOA,IAAa,WAAb,KAAwB,CACtB,UACA,eACA,MAAgB,GAChB,IAAwB,CAAC,EACzB,SAAmB,GACnB,eAAyB,GACzB,UAAoB,GACpB,QAAkB,GAClB,KAAe,EACf,QAAkB,EAElB,YAAY,EAAmB,EAAiB,GAAM,CACpD,KAAK,UAAYO,EACjB,KAAK,eAAiBC,CACxB,CAGA,KAAK,EAAc,EAAoD,CACrE,IAAK,IAAIH,EAAI,EAAGA,EAAIN,EAAK,OAAQ,GAAK,EAAG,CACvC,IAAIO,EAAOP,EAAKM,GACZ,KAAK,YACP,KAAK,UAAY,GACbC,IAAS;KAEV,KAAK,SAAWD,IAAM,GAAKC,IAAS,OACzC,KAAK,QAAU,GACXA,IAAS,OACX,KAAK,UAAY,GACjB,EAAO;GAEL,KAAK,UACHA,IAAS;IAAM,KAAK,MAAQ,GAC5BA,IAAS,IACPP,EAAKM,EAAI,KAAO,KAClB,KAAK,OAAS,IACd,GAAK,GAEL,KAAK,SAAW,GAGlB,KAAK,OAASC,GAIdA,IAAS,KAAO,CAAC,KAAK,gBAAkB,KAAK,QAAU,IACzD,KAAK,SAAW,GAChB,KAAK,eAAiB,IAGpBA,IAAS,IAEX,KAAK,OAAS,IAGZA,IAAS,KAAK,WAChB,KAAK,IAAI,KAAK,KAAK,KAAK,EACxB,KAAK,MAAQ,GACb,KAAK,eAAiB,IAGpBA,IAAS;GACX,KAAK,OAAOG,CAAK,EACjB,KAAK,eAAiB,IAGxB,KAAK,OAASH,EAChB,CACF,CAEA,OAAO,EAAoD,EACrD,KAAK,QAAU,IAAM,KAAK,IAAI,OAAS,GAAK,KAAK,iBAAgB,KAAK,OAAOG,CAAK,EACtF,KAAK,UAAY,EACnB,CAEA,OAAe,EAAoD,CACjE,KAAK,IAAI,KAAK,KAAK,KAAK,EACxB,KAAK,MAAQ,GACG,KAAK,IAAI,SAAW,GAAK,KAAK,IAAI,KAAO,IACxC,KAAK,gBAAgBA,EAAM,KAAK,IAAK,KAAK,OAAO,EAClE,KAAK,IAAM,CAAC,EACZ,KAAK,MAAQ,EACb,KAAK,QAAU,KAAK,IACtB,CACF,EAOA,SAAS,KAAK,EAAc,EAA6B,EAAoD,CAE3G,IAAMC,EAAU,IAAI,WADFjB,EAAQ,WAAa,gBAAgBM,CAAI,EACjBN,EAAQ,iBAAmB,EAAK,EAC1EiB,EAAQ,KAAKX,EAAMU,CAAK,EACxBC,EAAQ,OAAOD,CAAK,CACtB,CAMA,SAAgB,sBAAsB,EAAc,EAA+B,CAAC,EAAc,CAChG,GAAIhB,EAAQ,UAAY,IAAA,IAAaA,EAAQ,QAAQ,SAAW,EAC9D,MAAM,IAAI,iBAAiB,uCAAuC,EAEpE,IAAMkB,EAA4C,CAAC,EAEnD,GADA,KAAKZ,EAAMN,GAAU,EAAO,IAASkB,EAAK,KAAK,CAAE,QAAO,MAAK,CAAC,CAAC,EAC3DA,EAAK,SAAW,EAAG,MAAO,CAAC,EAC/B,IAAMC,EAAUnB,EAAQ,SAAakB,EAAK,EAAE,CAAyB,MAErE,OADalB,EAAQ,UAAY,IAAA,GAAYkB,EAAK,MAAM,CAAC,EAAIA,EAAA,CACjD,KAAK,CAAE,QAAO,UAAW,CACnC,IAAME,EAAiC,CAAC,EAKxC,OAJAD,EAAQ,SAAS,EAAQ,IAAU,CACjC,IAAME,EAAOC,EAAMC,GACfF,IAAS,IAAA,KAAW,EAAOG,GAAUH,EAC3C,CAAC,EACM,CAAE,OAAM,QAAO,CACxB,CAAC,CACH,CAEA,SAAgB,UAAU,EAAc,EAA8B,CAAC,EAAe,CACpF,IAAMH,EAAmB,CAAC,EAE1B,OADA,KAAKZ,EAAMN,EAAU,GAAQkB,EAAK,KAAKO,CAAG,CAAC,EACpCP,CACT,CAOA,SAAgB,aAAa,EAAc,EAA+B,CAAC,EAA6B,CACtG,OAAO,sBAAsBZ,EAAMN,CAAO,CAAC,CAAC,KAAK,CAAE,YAAaoB,CAAM,CACxE,CAEA,SAAS,YAAY,EAAe,EAA4B,CAC9D,OACEM,EAAM,SAASZ,CAAS,GACxBY,EAAM,SAAS,GAAG,GAClBA,EAAM,SAAS;CAAI,GACnBA,EAAM,SAAS,IAAI,GACnBA,IAAUA,EAAM,KAAK,CAEzB,CAEA,SAAS,cAAc,EAAc,EAAmB,EAA2B,CACjF,IAAMA,EAAQC,GAAQ,KAA4B,GAAK,OAAOA,CAAG,EAEjE,MADI,CAACC,GAAY,CAAC,YAAYF,EAAOZ,CAAS,EAAUY,EACjD,IAAIA,EAAM,WAAW,IAAK,IAAI,EAAE,EACzC,CAGA,SAAgB,MAAM,EAAoC,EAAkC,CAAC,EAAW,CACtG,IAAMZ,EAAYd,EAAQ,WAAa,IACjC6B,EAAU7B,EAAQ,SAAWI,EAC7BwB,EAAW5B,EAAQ,WAAa,GAChC8B,EAAkB,CAAC,EACzB,IAAK,IAAML,KAAOP,EAChBY,EAAM,KAAKL,EAAI,IAAK,GAAS,cAAcJ,EAAMP,EAAWc,CAAQ,CAAC,CAAC,CAAC,KAAKd,CAAS,CAAC,EAExF,OAAOgB,EAAM,KAAKD,CAAO,GAAKC,EAAM,OAAS,EAAID,EAAU,GAC7D,CAGA,SAAgB,aACd,EACA,EAAwC,CAAC,EACjC,CACR,IAAME,EAAO,CAAC,GAAGC,CAAO,EAClBC,EAAUjC,EAAQ,SAAW,CAAC,GAAG,IAAI,IAAI+B,EAAK,QAAS,GAAW,OAAO,KAAKX,CAAM,CAAC,CAAC,CAAC,EACvFF,EAA+B,CAACe,CAAO,EAC7C,IAAK,IAAMb,KAAUW,EAAMb,EAAK,KAAKe,EAAQ,IAAK,GAAWb,EAAOc,IAAW,EAAE,CAAC,EAClF,OAAO,MAAMhB,EAAMlB,CAAO,CAC5B,CCxMA,SAAgB,oBAAoB,EAA8B,CAAC,EAAsC,CACvG,IAAIc,EAAgCd,EAAQ,UACxCiB,EACFH,IAAc,IAAA,GAAY,IAAA,GAAY,IAAI,WAAWA,EAAWd,EAAQ,iBAAmB,EAAK,EAC9FmC,EAAU,GACR,mBACAlB,IAAY,IAAA,KACd,EAAY,gBAAgBkB,CAAO,EACnC,EAAU,IAAI,WAAWrB,EAAWd,EAAQ,iBAAmB,EAAK,GAE/DiB,GAET,OAAO,IAAI,gBAAkC,CAC3C,MAAM,EAAY,CAChB,IAAMmB,EAAS,cAAc,EACzBD,IAAY,IAAIC,EAAO,KAAKD,EAAU,GAAQE,EAAW,QAAQZ,CAAG,CAAC,EACzEW,EAAO,OAAQ,GAAQC,EAAW,QAAQZ,CAAG,CAAC,CAChD,EACA,UAAU,EAAO,EAAY,CAC3B,GAAIR,IAAY,IAAA,GAAW,CAGzB,GAFA,GAAWqB,EAEP,CAAC,SAAS,KAAKH,CAAO,EAAG,OAC7B,cAAc,CAAC,CAAC,KAAKA,EAAU,GAAQE,EAAW,QAAQZ,CAAG,CAAC,EAC9D,EAAU,EAEZ,MACAR,EAAQ,KAAKqB,EAAQ,GAAQD,EAAW,QAAQZ,CAAG,CAAC,CACtD,CACF,CAAC,CACH,CCvBA,SAAgB,kBACd,EACA,EACA,EAA+B,CAAC,EAC3B,CACL,IAAMc,EAAc,CAAC,EACfpC,EAA2B,CAAC,EAClC,IAAK,GAAM,CAAE,OAAM,YAAY,sBAAsBG,EAAMN,CAAO,EAAG,CACnE,IAAMwC,EAASC,EAAO,YAAY,CAAC,SAASrB,CAAM,EAClD,GAAIoB,aAAkB,QACpB,MAAU,UAAU,4DAA4D,EAE9EA,EAAO,SAAW,IAAA,GACpBD,EAAO,KAAKC,EAAO,KAAU,EAE7BrC,EAAO,KAAK,IAAI,eAAeF,EAAMuC,EAAO,MAAM,CAAC,CAEvD,CACA,GAAIrC,EAAO,OAAS,EAAG,MAAM,IAAI,sBAAsBA,CAAM,EAC7D,OAAOoC,CACT,C"}
|
|
@@ -0,0 +1,8 @@
|
|
|
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=!0){this.delimiter=e,this.skipEmptyLines=t}push(e,t){for(let n=0;n<e.length;n+=1){let r=e[n];this.pendingCr&&(this.pendingCr=!1,r===`
|
|
3
|
+
`)||(this.started||n!==0||r!==``)&&(this.started=!0,r===`\r`&&(this.pendingCr=!0,r=`
|
|
4
|
+
`),this.inQuotes?(r===`
|
|
5
|
+
`&&(this.line+=1),r===`"`?e[n+1]===`"`?(this.field+=`"`,n+=1):this.inQuotes=!1:this.field+=r):r===`"`&&!this.fieldWasQuoted&&this.field===``?(this.inQuotes=!0,this.fieldWasQuoted=!0):r===`"`?this.field+=`"`:r===this.delimiter?(this.row.push(this.field),this.field=``,this.fieldWasQuoted=!1):r===`
|
|
6
|
+
`?(this.endRow(t),this.fieldWasQuoted=!1):this.field+=r)}}finish(e){(this.field!==``||this.row.length>0||this.fieldWasQuoted)&&this.endRow(e),this.pendingCr=!1}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 scan(e,t,n){let r=new CsvScanner(t.delimiter??detectDelimiter(e),t.skipEmptyLines!==!1);r.push(e,n),r.finish(n)}function parseRecordsWithLines(e,t={}){if(t.headers!==void 0&&t.headers.length===0)throw new TabulaParseError(`headers must name at least one column`);let n=[];if(scan(e,t,(e,t)=>n.push({cells:e,line:t})),n.length===0)return[];let r=t.headers??n[0].cells;return(t.headers===void 0?n.slice(1):n).map(({cells:e,line:t})=>{let n={};return r.forEach((t,r)=>{let i=e[r];i!==void 0&&(n[t]=i)}),{line:t,record:n}})}function parseGrid(e,t={}){let n=[];return scan(e,t,e=>n.push(e)),n}function parseRecords(e,t={}){return parseRecordsWithLines(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=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=e.delimiter,n=t===void 0?void 0:new CsvScanner(t,e.skipEmptyLines!==!1),r=``,ensureScanner=()=>(n===void 0&&(t=detectDelimiter(r),n=new CsvScanner(t,e.skipEmptyLines!==!1)),n);return new TransformStream({flush(e){let t=ensureScanner();r!==``&&t.push(r,t=>e.enqueue(t)),t.finish(t=>e.enqueue(t))},transform(e,t){if(n===void 0){if(r+=e,!/[\n\r]/.test(r))return;ensureScanner().push(r,e=>t.enqueue(e)),r=``}else n.push(e,e=>t.enqueue(e))}})}function parseTypedRecords(e,t,n={}){let r=[],i=[];for(let{line:a,record:o}of parseRecordsWithLines(e,n)){let e=t[`~standard`].validate(o);if(e instanceof Promise)throw TypeError(`tabula: 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.detectDelimiter=detectDelimiter,e.parseGrid=parseGrid,e.parseRecords=parseRecords,e.parseTypedRecords=parseTypedRecords,e.recordsToCsv=recordsToCsv,e.toCsv=toCsv,e})({});
|
|
8
|
+
//# sourceMappingURL=tabula.iife.js.map
|
|
@@ -0,0 +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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"}
|
package/dist/tabula.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var TabulaError = class extends Error {
|
|
3
|
+
constructor(e, t) {
|
|
4
|
+
super(e, t), this.name = new.target.name, Object.setPrototypeOf(this, new.target.prototype);
|
|
5
|
+
}
|
|
6
|
+
}, TabulaParseError = class extends TabulaError {}, TabulaRowError = class extends TabulaError {
|
|
7
|
+
issues;
|
|
8
|
+
line;
|
|
9
|
+
constructor(e, t) {
|
|
10
|
+
super(`row at line ${e} failed validation`, { cause: t }), this.line = e, this.issues = t;
|
|
11
|
+
}
|
|
12
|
+
}, TabulaValidationError = class extends TabulaError {
|
|
13
|
+
errors;
|
|
14
|
+
constructor(e, t = `${e.length} invalid row(s)`) {
|
|
15
|
+
super(t, { cause: e }), this.errors = e;
|
|
16
|
+
}
|
|
17
|
+
}, e = "\r\n";
|
|
18
|
+
function detectDelimiter(e) {
|
|
19
|
+
let t = e.slice(0, 2048).split(/[\n\r]/, 1)[0] ?? "", n = ",", r = -1;
|
|
20
|
+
for (let e of [
|
|
21
|
+
" ",
|
|
22
|
+
";",
|
|
23
|
+
","
|
|
24
|
+
]) {
|
|
25
|
+
let i = 0, a = !1;
|
|
26
|
+
for (let n = 0; n < t.length; n += 1) {
|
|
27
|
+
let r = t[n];
|
|
28
|
+
r === "\"" ? a = !a : r === e && !a && (i += 1);
|
|
29
|
+
}
|
|
30
|
+
i > r && (n = e, r = i);
|
|
31
|
+
}
|
|
32
|
+
return r > 0 ? n : ",";
|
|
33
|
+
}
|
|
34
|
+
var CsvScanner = class {
|
|
35
|
+
delimiter;
|
|
36
|
+
skipEmptyLines;
|
|
37
|
+
field = "";
|
|
38
|
+
row = [];
|
|
39
|
+
inQuotes = !1;
|
|
40
|
+
fieldWasQuoted = !1;
|
|
41
|
+
pendingCr = !1;
|
|
42
|
+
started = !1;
|
|
43
|
+
line = 1;
|
|
44
|
+
rowLine = 1;
|
|
45
|
+
constructor(e, t = !0) {
|
|
46
|
+
this.delimiter = e, this.skipEmptyLines = t;
|
|
47
|
+
}
|
|
48
|
+
push(e, t) {
|
|
49
|
+
for (let n = 0; n < e.length; n += 1) {
|
|
50
|
+
let r = e[n];
|
|
51
|
+
this.pendingCr && (this.pendingCr = !1, r === "\n") || (this.started || n !== 0 || r !== "") && (this.started = !0, r === "\r" && (this.pendingCr = !0, r = "\n"), this.inQuotes ? (r === "\n" && (this.line += 1), r === "\"" ? e[n + 1] === "\"" ? (this.field += "\"", n += 1) : this.inQuotes = !1 : this.field += r) : r === "\"" && !this.fieldWasQuoted && this.field === "" ? (this.inQuotes = !0, this.fieldWasQuoted = !0) : r === "\"" ? this.field += "\"" : r === this.delimiter ? (this.row.push(this.field), this.field = "", this.fieldWasQuoted = !1) : r === "\n" ? (this.endRow(t), this.fieldWasQuoted = !1) : this.field += r);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finish(e) {
|
|
55
|
+
(this.field !== "" || this.row.length > 0 || this.fieldWasQuoted) && this.endRow(e), this.pendingCr = !1;
|
|
56
|
+
}
|
|
57
|
+
endRow(e) {
|
|
58
|
+
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
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function scan(e, t, n) {
|
|
62
|
+
let r = new CsvScanner(t.delimiter ?? detectDelimiter(e), t.skipEmptyLines !== !1);
|
|
63
|
+
r.push(e, n), r.finish(n);
|
|
64
|
+
}
|
|
65
|
+
function parseRecordsWithLines(e, t = {}) {
|
|
66
|
+
if (t.headers !== void 0 && t.headers.length === 0) throw new TabulaParseError("headers must name at least one column");
|
|
67
|
+
let n = [];
|
|
68
|
+
if (scan(e, t, (e, t) => n.push({
|
|
69
|
+
cells: e,
|
|
70
|
+
line: t
|
|
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 }) => {
|
|
74
|
+
let n = {};
|
|
75
|
+
return r.forEach((t, r) => {
|
|
76
|
+
let i = e[r];
|
|
77
|
+
i !== void 0 && (n[t] = i);
|
|
78
|
+
}), {
|
|
79
|
+
line: t,
|
|
80
|
+
record: n
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function parseGrid(e, t = {}) {
|
|
85
|
+
let n = [];
|
|
86
|
+
return scan(e, t, (e) => n.push(e)), n;
|
|
87
|
+
}
|
|
88
|
+
function parseRecords(e, t = {}) {
|
|
89
|
+
return parseRecordsWithLines(e, t).map(({ record: e }) => e);
|
|
90
|
+
}
|
|
91
|
+
function needsQuotes(e, t) {
|
|
92
|
+
return e.includes(t) || e.includes("\"") || e.includes("\n") || e.includes("\r") || e !== e.trim();
|
|
93
|
+
}
|
|
94
|
+
function serializeCell(e, t, n) {
|
|
95
|
+
let r = e == null ? "" : String(e);
|
|
96
|
+
return !n && !needsQuotes(r, t) ? r : `"${r.replaceAll("\"", "\"\"")}"`;
|
|
97
|
+
}
|
|
98
|
+
function toCsv(t, n = {}) {
|
|
99
|
+
let r = n.delimiter ?? ",", i = n.newline ?? e, a = n.quoteAll === !0, o = [];
|
|
100
|
+
for (let e of t) o.push(e.map((e) => serializeCell(e, r, a)).join(r));
|
|
101
|
+
return o.join(i) + (o.length > 0 ? i : "");
|
|
102
|
+
}
|
|
103
|
+
function recordsToCsv(e, t = {}) {
|
|
104
|
+
let n = [...e], r = t.columns ?? [...new Set(n.flatMap((e) => Object.keys(e)))], i = [r];
|
|
105
|
+
for (let e of n) i.push(r.map((t) => e[t] ?? ""));
|
|
106
|
+
return toCsv(i, t);
|
|
107
|
+
}
|
|
108
|
+
// src/stream.ts
|
|
109
|
+
function createCsvGridStream(e = {}) {
|
|
110
|
+
let t = e.delimiter, n = t === void 0 ? void 0 : new CsvScanner(t, e.skipEmptyLines !== !1), r = "", ensureScanner = () => (n === void 0 && (t = detectDelimiter(r), n = new CsvScanner(t, e.skipEmptyLines !== !1)), n);
|
|
111
|
+
return new TransformStream({
|
|
112
|
+
flush(e) {
|
|
113
|
+
let t = ensureScanner();
|
|
114
|
+
r !== "" && t.push(r, (t) => e.enqueue(t)), t.finish((t) => e.enqueue(t));
|
|
115
|
+
},
|
|
116
|
+
transform(e, t) {
|
|
117
|
+
if (n === void 0) {
|
|
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));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
// src/typed.ts
|
|
125
|
+
function parseTypedRecords(e, t, n = {}) {
|
|
126
|
+
let r = [], i = [];
|
|
127
|
+
for (let { line: a, record: o } of parseRecordsWithLines(e, n)) {
|
|
128
|
+
let e = t["~standard"].validate(o);
|
|
129
|
+
if (e instanceof Promise) throw TypeError("tabula: async schemas are not supported; use a sync schema");
|
|
130
|
+
e.issues === void 0 ? r.push(e.value) : i.push(new TabulaRowError(a, e.issues));
|
|
131
|
+
}
|
|
132
|
+
if (i.length > 0) throw new TabulaValidationError(i);
|
|
133
|
+
return r;
|
|
134
|
+
}
|
|
135
|
+
export { CsvScanner, TabulaError, TabulaParseError, TabulaRowError, TabulaValidationError, createCsvGridStream, detectDelimiter, parseGrid, parseRecords, parseTypedRecords, recordsToCsv, toCsv };
|
|
136
|
+
|
|
137
|
+
//# sourceMappingURL=tabula.js.map
|
|
@@ -0,0 +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":";AACA,IAAa,cAAb,cAAiC,MAAM;CACrC,YAAY,GAAiB,GAAwB;EAGnD,AAFA,MAAMA,GAASC,CAAO,GACtB,KAAK,OAAO,WAAW,MACvB,OAAO,eAAe,MAAM,WAAW,SAAS;CAClD;AACF,GAGa,mBAAb,cAAsC,YAAY,CAAC,GAGtC,iBAAb,cAAoC,YAAY;CAC9C;CACA;CAEA,YAAY,GAAc,GAA4B;EAGpD,AAFA,MAAM,eAAeC,EAAK,qBAAqB,EAAE,OAAOC,EAAO,CAAC,GAChE,KAAK,OAAOD,GACZ,KAAK,SAASC;CAChB;AACF,GAGa,wBAAb,cAA2C,YAAY;CACrD;CAEA,YAAY,GAAmC,IAAU,GAAGC,EAAO,OAAO,kBAAkB;EAE1F,AADA,MAAMJ,GAAS,EAAE,OAAOI,EAAO,CAAC,GAChC,KAAK,SAASA;CAChB;AACF,GCxBMC,IAAkB;AAGxB,SAAgB,gBAAgB,GAAsB;CACpD,IAAMC,IAAYC,EAAK,MAAM,GAAG,IAAK,CAAC,CAAC,MAAM,UAAU,CAAC,CAAC,CAAC,MAAM,IAC5DC,IAAO,KACPC,IAAY;CAChB,KAAK,IAAMC,KAAa;EAAC;EAAM;EAAK;CAAG,GAAG;EACxC,IAAIC,IAAQ,GACRC,IAAS;EACb,KAAK,IAAIC,IAAI,GAAGA,IAAIP,EAAU,QAAQ,KAAK,GAAG;GAC5C,IAAMQ,IAAOR,EAAUO;GACvB,AAAIC,MAAS,OAAK,IAAS,CAACF,IACnBE,MAASJ,KAAa,CAACE,MAAQ,KAAS;EACnD;EACA,AAAID,IAAQF,MACV,IAAOC,GACP,IAAYC;CAEhB;CACA,OAAOF,IAAY,IAAID,IAAO;AAChC;AAOA,IAAa,aAAb,MAAwB;CACtB;CACA;CACA,QAAgB;CAChB,MAAwB,CAAC;CACzB,WAAmB;CACnB,iBAAyB;CACzB,YAAoB;CACpB,UAAkB;CAClB,OAAe;CACf,UAAkB;CAElB,YAAY,GAAmB,IAAiB,IAAM;EAEpD,AADA,KAAK,YAAYO,GACjB,KAAK,iBAAiBC;CACxB;CAGA,KAAK,GAAc,GAAoD;EACrE,KAAK,IAAIH,IAAI,GAAGA,IAAIN,EAAK,QAAQ,KAAK,GAAG;GACvC,IAAIO,IAAOP,EAAKM;GAChB,AAAI,KAAK,cACP,KAAK,YAAY,IACbC,MAAS,UAEV,KAAK,WAAWD,MAAM,KAAKC,MAAS,SACzC,KAAK,UAAU,IACXA,MAAS,SACX,KAAK,YAAY,IACjB,IAAO,OAEL,KAAK,YACHA,MAAS,SAAM,KAAK,QAAQ,IAC5BA,MAAS,OACPP,EAAKM,IAAI,OAAO,QAClB,KAAK,SAAS,MACd,KAAK,KAEL,KAAK,WAAW,KAGlB,KAAK,SAASC,KAIdA,MAAS,QAAO,CAAC,KAAK,kBAAkB,KAAK,UAAU,MACzD,KAAK,WAAW,IAChB,KAAK,iBAAiB,MAGpBA,MAAS,OAEX,KAAK,SAAS,OAGZA,MAAS,KAAK,aAChB,KAAK,IAAI,KAAK,KAAK,KAAK,GACxB,KAAK,QAAQ,IACb,KAAK,iBAAiB,MAGpBA,MAAS,QACX,KAAK,OAAOG,CAAK,GACjB,KAAK,iBAAiB,MAGxB,KAAK,SAASH;EAChB;CACF;CAEA,OAAO,GAAoD;EAEzD,CADI,KAAK,UAAU,MAAM,KAAK,IAAI,SAAS,KAAK,KAAK,mBAAgB,KAAK,OAAOG,CAAK,GACtF,KAAK,YAAY;CACnB;CAEA,OAAe,GAAoD;EAOjE,AANA,KAAK,IAAI,KAAK,KAAK,KAAK,GACxB,KAAK,QAAQ,IACG,KAAK,IAAI,WAAW,KAAK,KAAK,IAAI,OAAO,MACxC,KAAK,kBAAgBA,EAAM,KAAK,KAAK,KAAK,OAAO,GAClE,KAAK,MAAM,CAAC,GACZ,KAAK,QAAQ,GACb,KAAK,UAAU,KAAK;CACtB;AACF;AAOA,SAAS,KAAK,GAAc,GAA6B,GAAoD;CAE3G,IAAMC,IAAU,IAAI,WADFjB,EAAQ,aAAa,gBAAgBM,CAAI,GACjBN,EAAQ,mBAAmB,EAAK;CAE1E,AADAiB,EAAQ,KAAKX,GAAMU,CAAK,GACxBC,EAAQ,OAAOD,CAAK;AACtB;AAMA,SAAgB,sBAAsB,GAAc,IAA+B,CAAC,GAAc;CAChG,IAAIhB,EAAQ,YAAY,KAAA,KAAaA,EAAQ,QAAQ,WAAW,GAC9D,MAAM,IAAI,iBAAiB,uCAAuC;CAEpE,IAAMkB,IAA4C,CAAC;CAEnD,IADA,KAAKZ,GAAMN,IAAU,GAAO,MAASkB,EAAK,KAAK;EAAE;EAAO;CAAK,CAAC,CAAC,GAC3DA,EAAK,WAAW,GAAG,OAAO,CAAC;CAC/B,IAAMC,IAAUnB,EAAQ,WAAakB,EAAK,EAAE,CAAyB;CAErE,QADalB,EAAQ,YAAY,KAAA,IAAYkB,EAAK,MAAM,CAAC,IAAIA,EAAA,CACjD,KAAK,EAAE,UAAO,cAAW;EACnC,IAAME,IAAiC,CAAC;EAKxC,OAJAD,EAAQ,SAAS,GAAQ,MAAU;GACjC,IAAME,IAAOC,EAAMC;GACnB,AAAIF,MAAS,KAAA,MAAW,EAAOG,KAAUH;EAC3C,CAAC,GACM;GAAE;GAAM;EAAO;CACxB,CAAC;AACH;AAEA,SAAgB,UAAU,GAAc,IAA8B,CAAC,GAAe;CACpF,IAAMH,IAAmB,CAAC;CAE1B,OADA,KAAKZ,GAAMN,IAAU,MAAQkB,EAAK,KAAKO,CAAG,CAAC,GACpCP;AACT;AAOA,SAAgB,aAAa,GAAc,IAA+B,CAAC,GAA6B;CACtG,OAAO,sBAAsBZ,GAAMN,CAAO,CAAC,CAAC,KAAK,EAAE,gBAAaoB,CAAM;AACxE;AAEA,SAAS,YAAY,GAAe,GAA4B;CAC9D,OACEM,EAAM,SAASZ,CAAS,KACxBY,EAAM,SAAS,IAAG,KAClBA,EAAM,SAAS,IAAI,KACnBA,EAAM,SAAS,IAAI,KACnBA,MAAUA,EAAM,KAAK;AAEzB;AAEA,SAAS,cAAc,GAAc,GAAmB,GAA2B;CACjF,IAAMA,IAAQC,KAAQ,OAA4B,KAAK,OAAOA,CAAG;CAEjE,OADI,CAACC,KAAY,CAAC,YAAYF,GAAOZ,CAAS,IAAUY,IACjD,IAAIA,EAAM,WAAW,MAAK,MAAI,EAAE;AACzC;AAGA,SAAgB,MAAM,GAAoC,IAAkC,CAAC,GAAW;CACtG,IAAMZ,IAAYd,EAAQ,aAAa,KACjC6B,IAAU7B,EAAQ,WAAWI,GAC7BwB,IAAW5B,EAAQ,aAAa,IAChC8B,IAAkB,CAAC;CACzB,KAAK,IAAML,KAAOP,GAChB,EAAM,KAAKO,EAAI,KAAK,MAAS,cAAcJ,GAAMP,GAAWc,CAAQ,CAAC,CAAC,CAAC,KAAKd,CAAS,CAAC;CAExF,OAAOgB,EAAM,KAAKD,CAAO,KAAKC,EAAM,SAAS,IAAID,IAAU;AAC7D;AAGA,SAAgB,aACd,GACA,IAAwC,CAAC,GACjC;CACR,IAAME,IAAO,CAAC,GAAGC,CAAO,GAClBC,IAAUjC,EAAQ,WAAW,CAAC,GAAG,IAAI,IAAI+B,EAAK,SAAS,MAAW,OAAO,KAAKX,CAAM,CAAC,CAAC,CAAC,GACvFF,IAA+B,CAACe,CAAO;CAC7C,KAAK,IAAMb,KAAUW,GAAM,EAAK,KAAKE,EAAQ,KAAK,MAAWb,EAAOc,MAAW,EAAE,CAAC;CAClF,OAAO,MAAMhB,GAAMlB,CAAO;AAC5B;;ACxMA,SAAgB,oBAAoB,IAA8B,CAAC,GAAsC;CACvG,IAAIc,IAAgCd,EAAQ,WACxCiB,IACFH,MAAc,KAAA,IAAY,KAAA,IAAY,IAAI,WAAWA,GAAWd,EAAQ,mBAAmB,EAAK,GAC9FmC,IAAU,IACR,uBACAlB,MAAY,KAAA,MACd,IAAY,gBAAgBkB,CAAO,GACnC,IAAU,IAAI,WAAWrB,GAAWd,EAAQ,mBAAmB,EAAK,IAE/DiB;CAET,OAAO,IAAI,gBAAkC;EAC3C,MAAM,GAAY;GAChB,IAAMmB,IAAS,cAAc;GAE7B,AADID,MAAY,MAAIC,EAAO,KAAKD,IAAU,MAAQE,EAAW,QAAQZ,CAAG,CAAC,GACzEW,EAAO,QAAQ,MAAQC,EAAW,QAAQZ,CAAG,CAAC;EAChD;EACA,UAAU,GAAO,GAAY;GAC3B,IAAIR,MAAY,KAAA,GAAW;IAGzB,IAFA,KAAWqB,GAEP,CAAC,SAAS,KAAKH,CAAO,GAAG;IAE7B,AADA,cAAc,CAAC,CAAC,KAAKA,IAAU,MAAQE,EAAW,QAAQZ,CAAG,CAAC,GAC9D,IAAU;GAEZ,OACA,EAAQ,KAAKa,IAAQ,MAAQD,EAAW,QAAQZ,CAAG,CAAC;EACtD;CACF,CAAC;AACH;;ACvBA,SAAgB,kBACd,GACA,GACA,IAA+B,CAAC,GAC3B;CACL,IAAMc,IAAc,CAAC,GACfpC,IAA2B,CAAC;CAClC,KAAK,IAAM,EAAE,SAAM,eAAY,sBAAsBG,GAAMN,CAAO,GAAG;EACnE,IAAMwC,IAASC,EAAO,YAAY,CAAC,SAASrB,CAAM;EAClD,IAAIoB,aAAkB,SACpB,MAAU,UAAU,4DAA4D;EAElF,AAAIA,EAAO,WAAW,KAAA,IACpBD,EAAO,KAAKC,EAAO,KAAU,IAE7BrC,EAAO,KAAK,IAAI,eAAeF,GAAMuC,EAAO,MAAM,CAAC;CAEvD;CACA,IAAIrC,EAAO,SAAS,GAAG,MAAM,IAAI,sBAAsBA,CAAM;CAC7D,OAAOoC;AACT"}
|
package/dist/typed.cjs
ADDED
|
@@ -0,0 +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.parseRecordsWithLines(n,i)){let e=r[`~standard`].validate(c);if(e instanceof Promise)throw TypeError(`tabula: 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
|
+
//# sourceMappingURL=typed.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typed.cjs","names":["parseRecordsWithLines","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 { 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":"A,uDAiBA,SAAgB,kBACd,EACA,EACA,EAA+B,CAAC,EAC3B,CACL,IAAMK,EAAc,CAAC,EACfC,EAA2B,CAAC,EAClC,IAAK,GAAM,CAAE,OAAM,YAAYN,EAAAA,sBAAsBO,EAAMC,CAAO,EAAG,CACnE,IAAMC,EAASC,EAAO,YAAY,CAAC,SAASC,CAAM,EAClD,GAAIF,aAAkB,QACpB,MAAU,UAAU,4DAA4D,EAE9EA,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.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@vielzeug/spell';
|
|
2
|
+
import type { TabulaRecordOptions } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Parse CSV text into typed rows validated by a Standard Schema (v1) — every
|
|
5
|
+
* `@vielzeug/spell` schema qualifies. Combine with spell's coercion builders
|
|
6
|
+
* (`s.coerce.number()` …) to turn the raw string cells into numbers, booleans,
|
|
7
|
+
* or dates in the same pass.
|
|
8
|
+
*
|
|
9
|
+
* All rows are validated before returning: on any failure a
|
|
10
|
+
* {@link TabulaValidationError} collects one {@link TabulaRowError} (1-based
|
|
11
|
+
* `line` + schema issues) per invalid row, so a bad export is reported in full
|
|
12
|
+
* instead of at the first bad cell.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseTypedRecords<T>(text: string, schema: StandardSchemaV1<Record<string, unknown>, T>, options?: TabulaRecordOptions): T[];
|
|
15
|
+
//# sourceMappingURL=typed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typed.d.ts","sourceRoot":"","sources":["../src/typed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAIxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,gBAAgB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,EACpD,OAAO,GAAE,mBAAwB,GAChC,CAAC,EAAE,CAgBL"}
|
package/dist/typed.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { parseRecordsWithLines as e } from "./csv.js";
|
|
2
|
+
import { TabulaRowError as t, TabulaValidationError as n } from "./errors.js";
|
|
3
|
+
// src/typed.ts
|
|
4
|
+
function parseTypedRecords(r, i, a = {}) {
|
|
5
|
+
let o = [], s = [];
|
|
6
|
+
for (let { line: n, record: c } of e(r, a)) {
|
|
7
|
+
let e = i["~standard"].validate(c);
|
|
8
|
+
if (e instanceof Promise) throw TypeError("tabula: async schemas are not supported; use a sync schema");
|
|
9
|
+
e.issues === void 0 ? o.push(e.value) : s.push(new t(n, e.issues));
|
|
10
|
+
}
|
|
11
|
+
if (s.length > 0) throw new n(s);
|
|
12
|
+
return o;
|
|
13
|
+
}
|
|
14
|
+
export { parseTypedRecords };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=typed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typed.js","names":["values","errors","parseRecordsWithLines","text","options","result","schema","record","TabulaRowError","line","TabulaValidationError"],"sources":["../src/typed.ts"],"sourcesContent":["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":";;;AAiBA,SAAgB,kBACd,GACA,GACA,IAA+B,CAAC,GAC3B;CACL,IAAMA,IAAc,CAAC,GACfC,IAA2B,CAAC;CAClC,KAAK,IAAM,EAAE,SAAM,eAAYC,EAAsBC,GAAMC,CAAO,GAAG;EACnE,IAAMC,IAASC,EAAO,YAAY,CAAC,SAASC,CAAM;EAClD,IAAIF,aAAkB,SACpB,MAAU,UAAU,4DAA4D;EAElF,AAAIA,EAAO,WAAW,KAAA,IACpBL,EAAO,KAAKK,EAAO,KAAU,IAE7BJ,EAAO,KAAK,IAAIO,EAAeC,GAAMJ,EAAO,MAAM,CAAC;CAEvD;CACA,IAAIJ,EAAO,SAAS,GAAG,MAAM,IAAIS,EAAsBT,CAAM;CAC7D,OAAOD;AACT"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Shared options for tabula parsers and serializers. */
|
|
2
|
+
export type TabulaOptions = {
|
|
3
|
+
/** Field delimiter; defaults to auto-detection among `,`, `;`, and tab from the first line. */
|
|
4
|
+
delimiter?: string;
|
|
5
|
+
};
|
|
6
|
+
/** Parser options for the text and stream readers. */
|
|
7
|
+
export type TabulaParseOptions = TabulaOptions & {
|
|
8
|
+
/** Skip blank lines instead of surfacing them as empty rows (default `true`). */
|
|
9
|
+
skipEmptyLines?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** Options for reading a header row into records. */
|
|
12
|
+
export type TabulaRecordOptions = TabulaParseOptions & {
|
|
13
|
+
/** Header names to use instead of the first row; the first row is then data. */
|
|
14
|
+
headers?: readonly string[];
|
|
15
|
+
};
|
|
16
|
+
/** Serializer options. */
|
|
17
|
+
export type TabulaSerializeOptions = TabulaOptions & {
|
|
18
|
+
/** Quote every field regardless of need (default `false`). */
|
|
19
|
+
quoteAll?: boolean;
|
|
20
|
+
/** Line terminator (default RFC 4180 `\r\n`). */
|
|
21
|
+
newline?: string;
|
|
22
|
+
};
|
|
23
|
+
/** Options for record serialization; insertion order of the first record defines columns unless overridden. */
|
|
24
|
+
export type TabulaRecordSerializeOptions = TabulaSerializeOptions & {
|
|
25
|
+
/** Explicit column order. */
|
|
26
|
+
columns?: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +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,+GAA+G;AAC/G,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,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vielzeug/tabula",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "26.11.13",
|
|
4
|
+
"description": "Typed CSV and TSV: RFC 4180 parser with schema-validated rows, streaming reader, and safe serializer.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/helmuthdu/vielzeug",
|
|
8
|
+
"directory": "packages/tabula"
|
|
9
|
+
},
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs",
|
|
25
|
+
"types": "./dist/index.d.ts"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public",
|
|
31
|
+
"registry": "https://registry.npmjs.org/"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "vite build && bun run --silent build:bundle && bun run --silent build:types",
|
|
35
|
+
"build:bundle": "vite build --config vite.bundle.config.ts",
|
|
36
|
+
"build:types": "tsc -p tsconfig.declarations.json",
|
|
37
|
+
"fix": "biome check --write src",
|
|
38
|
+
"lint": "biome ci src",
|
|
39
|
+
"prepublishOnly": "bun run --silent build",
|
|
40
|
+
"test": "vitest"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@vielzeug/spell": "26.11.13"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^26.6.6",
|
|
47
|
+
"typescript": "^6.0.3",
|
|
48
|
+
"vite": "^8.3.6",
|
|
49
|
+
"vitest": "^5.1.0"
|
|
50
|
+
}
|
|
51
|
+
}
|