@jarenjs/josl 0.34.0

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.
@@ -0,0 +1,196 @@
1
+ //#region CSV streaming reader and writer
2
+ // Chunk-feedable reader for incremental input — a network response, a
3
+ // file read in pieces, an LLM emitting a table token by token. Rows are
4
+ // complete the moment their record terminates, so a consumer can work on
5
+ // row 1 while row 100000 is still on the wire.
6
+ //
7
+ // The reader drives the same machine as `parseCsv`, so a document read in
8
+ // chunks and the same document read whole produce identical rows; the
9
+ // test suite asserts exactly that at every chunk size.
10
+
11
+ import { CsvMachine } from './csv-machine.js';
12
+ import { stringifyCsvChunks, formatCsvValue } from './csv.js';
13
+
14
+ //#region reading
15
+
16
+ /**
17
+ * Create an incremental CSV reader.
18
+ *
19
+ * Events, in document order:
20
+ * {type:'header', fields, line} - column names resolved
21
+ * {type:'row', index, values, record, line} - a record completed
22
+ * {type:'repair', code, message, line, column} - damage healed
23
+ *
24
+ * @param {object} [options] - Reader options; see `parseCsv`
25
+ * @returns {{feed(chunk: string): void, end(): Array, rows(): Array,
26
+ * fields(): string[]|null, repairs(): object[]}} The reader: `feed`
27
+ * accepts chunks that may split any field, `end` flushes the pending
28
+ * record and returns every row, and the rest peek at partial results.
29
+ * @example
30
+ * const reader = createCsvStreamReader({ headers: true, onEvent: console.log });
31
+ * reader.feed('a,b\n1,');
32
+ * reader.feed('2\n');
33
+ * reader.end(); // [{ a: '1', b: '2' }]
34
+ */
35
+ export function createCsvStreamReader(options = undefined) {
36
+ const machine = new CsvMachine(options);
37
+ return {
38
+ feed(chunk) {
39
+ machine.feed(chunk);
40
+ },
41
+ end() {
42
+ return machine.end();
43
+ },
44
+ rows() {
45
+ return machine.rows();
46
+ },
47
+ fields() {
48
+ return machine.fields();
49
+ },
50
+ repairs() {
51
+ return machine.repairs();
52
+ },
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Read an async iterable of string chunks (a fetch body, a file stream,
58
+ * an LLM response) into records.
59
+ * @param {AsyncIterable<string>|Iterable<string>} chunks - Source chunks
60
+ * @param {object} [options] - Reader options; see `parseCsv`
61
+ * @returns {Promise<Array>} Every record
62
+ */
63
+ export async function parseCsvStream(chunks, options = undefined) {
64
+ const machine = new CsvMachine(options);
65
+ for await (const chunk of chunks)
66
+ machine.feed(chunk);
67
+ return machine.end();
68
+ }
69
+
70
+ /**
71
+ * Yield records as they complete, without ever holding the whole table.
72
+ *
73
+ * This is the reason to stream: `parseCsvStream` still accumulates every
74
+ * row, so a file larger than memory needs a reader that hands each record
75
+ * over and forgets it. Rows are dropped from the machine as they are
76
+ * yielded, so memory stays flat in the number of records.
77
+ * @param {AsyncIterable<string>|Iterable<string>} chunks - Source chunks
78
+ * @param {object} [options] - Reader options; see `parseCsv`
79
+ * @yields {Array|object} One record at a time
80
+ * @example
81
+ * for await (const row of iterateCsvStream(response.body, { headers: true }))
82
+ * await save(row);
83
+ */
84
+ export async function* iterateCsvStream(chunks, options = undefined) {
85
+ const machine = new CsvMachine(options);
86
+ const pending = machine.rows();
87
+ for await (const chunk of chunks) {
88
+ machine.feed(chunk);
89
+ if (pending.length !== 0) {
90
+ // hand over the completed rows and drop them, so the machine never
91
+ // accumulates the document it is streaming
92
+ const batch = pending.splice(0, pending.length);
93
+ for (const row of batch)
94
+ yield row;
95
+ }
96
+ }
97
+ machine.end();
98
+ for (const row of pending.splice(0, pending.length))
99
+ yield row;
100
+ }
101
+
102
+ //#endregion
103
+
104
+ //#region writing
105
+
106
+ /**
107
+ * An incremental CSV writer: rows in, chunks out.
108
+ */
109
+ export class CsvStreamWriter {
110
+ /**
111
+ * @param {object} [options] - Writer options; see `stringifyCsv`
112
+ */
113
+ constructor(options = {}) {
114
+ this.options = options;
115
+ this.chunks = [];
116
+ this.onChunk = options.onChunk ?? null;
117
+ this.fields = options.fields ?? null;
118
+ this.wroteHeader = options.header === false;
119
+ }
120
+
121
+ #emit(text) {
122
+ if (this.onChunk !== null)
123
+ this.onChunk(text);
124
+ else
125
+ this.chunks.push(text);
126
+ return this;
127
+ }
128
+
129
+ /**
130
+ * Write one record.
131
+ * @param {Array|object} row - An array, or an object keyed by column
132
+ * @returns {this} The writer, for chaining
133
+ */
134
+ write(row) {
135
+ if (!Array.isArray(row) && this.fields === null)
136
+ this.fields = Object.keys(row);
137
+ for (const chunk of stringifyCsvChunks([row], {
138
+ ...this.options,
139
+ fields: this.fields,
140
+ header: !this.wroteHeader && this.options.header !== false,
141
+ })) {
142
+ this.#emit(chunk);
143
+ }
144
+ if (!Array.isArray(row))
145
+ this.wroteHeader = true;
146
+ return this;
147
+ }
148
+
149
+ /**
150
+ * Write many records.
151
+ * @param {Iterable<Array|object>} rows - Records
152
+ * @returns {this} The writer, for chaining
153
+ */
154
+ writeAll(rows) {
155
+ for (const row of rows)
156
+ this.write(row);
157
+ return this;
158
+ }
159
+
160
+ /**
161
+ * Concatenate the buffered chunks; empty when an `onChunk` sink is set.
162
+ * @returns {string} The document so far
163
+ */
164
+ toString() {
165
+ return this.chunks.join('');
166
+ }
167
+
168
+ /**
169
+ * Finish writing.
170
+ * @returns {string} The complete document, or `''` with an `onChunk` sink
171
+ */
172
+ end() {
173
+ return this.toString();
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Create an incremental CSV writer.
179
+ * @param {object} [options] - Writer options; see `stringifyCsv`
180
+ * @param {(chunk: string) => void} [options.onChunk] - Chunk sink; without
181
+ * one, chunks buffer until `end()`
182
+ * @returns {CsvStreamWriter} The writer
183
+ * @example
184
+ * const w = createCsvStreamWriter();
185
+ * w.write({ a: 1, b: 2 }).write({ a: 3, b: 4 });
186
+ * w.end(); // 'a,b\r\n1,2\r\n3,4\r\n'
187
+ */
188
+ export function createCsvStreamWriter(options = undefined) {
189
+ return new CsvStreamWriter(options ?? {});
190
+ }
191
+
192
+ //#endregion
193
+
194
+ export { stringifyCsvChunks, formatCsvValue };
195
+
196
+ //#endregion
package/src/csv.js ADDED
@@ -0,0 +1,363 @@
1
+ //#region CSV whole-document API
2
+ // Reading and writing a complete CSV document. The reader is the same
3
+ // machine the streaming reader drives (csv-machine.js), so a document
4
+ // parses identically whether it arrives at once or in chunks.
5
+
6
+ import { CsvMachine, CSV_CODES, coerceCsvValue } from './csv-machine.js';
7
+ import { CsvSyntaxError } from './errors.js';
8
+
9
+ //#region reading
10
+
11
+ /**
12
+ * Parse a complete CSV document.
13
+ *
14
+ * Default is **strict**: anything RFC 4180 does not allow throws a
15
+ * `CsvSyntaxError` carrying a `CSV1xxx` code, a line and a column.
16
+ * `repair: true` instead takes the reading that loses the least data,
17
+ * records it, and carries on — see `parseCsvDocument` for the log.
18
+ *
19
+ * @param {string} text - CSV source text
20
+ * @param {object} [options] - Reader options
21
+ * @param {string} [options.delimiter=','] - Field separator, one character
22
+ * @param {string|null} [options.quote='"'] - Quote character; `null` disables quoting
23
+ * @param {string|null} [options.comment] - Lines starting with it are skipped
24
+ * @param {boolean|string[]} [options.headers=false] - `true` reads the first
25
+ * record as column names, an array supplies them; either way records become
26
+ * objects. `false` yields arrays, which allocate less.
27
+ * @param {boolean} [options.repair=false] - Heal damage instead of throwing
28
+ * @param {boolean} [options.trim=false] - Trim spaces/tabs around unquoted fields
29
+ * @param {boolean} [options.typed=false] - Coerce numbers, bigints, booleans,
30
+ * `null` and ISO dates; quoted cells always stay strings
31
+ * @param {boolean} [options.emptyAsNull=false] - Empty cells become `null`
32
+ * @param {boolean} [options.skipEmptyLines] - Drop blank lines (default: on
33
+ * in repair mode, off otherwise)
34
+ * @param {(event: object) => void} [options.onEvent] - Document-order event sink
35
+ * @param {(repair: object) => void} [options.onRepair] - Repair sink
36
+ * @returns {Array<string[]|object>} The records
37
+ * @throws {CsvSyntaxError} On malformed input, unless `repair` is set
38
+ * @example
39
+ * parseCsv('a,b\n1,2'); // [['a','b'], ['1','2']]
40
+ * parseCsv('a,b\n1,2', { headers: true }); // [{ a: '1', b: '2' }]
41
+ */
42
+ export function parseCsv(text, options = undefined) {
43
+ return new CsvMachine(options).parseAll(text);
44
+ }
45
+
46
+ /**
47
+ * Parse a complete CSV document and report everything that was learned
48
+ * about it: the resolved dialect, the column names, the records, and the
49
+ * repair log. This is the form to use when the input is untrusted — it is
50
+ * the only way to see *what* had to be healed.
51
+ * @param {string} text - CSV source text
52
+ * @param {object} [options] - Reader options; see `parseCsv`. `delimiter:
53
+ * 'auto'` sniffs the dialect from the text first.
54
+ * @returns {{dialect: object, fields: string[]|null, rows: Array, repairs: object[]}}
55
+ * The document
56
+ * @example
57
+ * const doc = parseCsvDocument('a;b\n1;2', { delimiter: 'auto', headers: true });
58
+ * doc.dialect.delimiter; // ';'
59
+ * doc.rows; // [{ a: '1', b: '2' }]
60
+ */
61
+ export function parseCsvDocument(text, options = undefined) {
62
+ let opts = options ?? {};
63
+ let dialect;
64
+ if (opts.delimiter === 'auto') {
65
+ dialect = sniffCsvDialect(text, opts);
66
+ opts = { ...opts, delimiter: dialect.delimiter };
67
+ if (opts.headers === 'auto')
68
+ opts.headers = dialect.headers;
69
+ }
70
+ else {
71
+ if (opts.headers === 'auto')
72
+ opts = { ...opts, headers: sniffCsvDialect(text, opts).headers };
73
+ dialect = {
74
+ delimiter: opts.delimiter ?? ',',
75
+ quote: opts.quote === undefined ? '"' : opts.quote,
76
+ headers: opts.headers === true || Array.isArray(opts.headers),
77
+ confidence: 1,
78
+ };
79
+ }
80
+ const machine = new CsvMachine(opts);
81
+ const rows = machine.parseAll(text);
82
+ return { dialect, fields: machine.fields(), rows, repairs: machine.repairs() };
83
+ }
84
+
85
+ //#endregion
86
+
87
+ //#region dialect sniffing
88
+
89
+ const CANDIDATES = [',', ';', '\t', '|', ':'];
90
+ const SNIFF_BYTES = 65536;
91
+ const SNIFF_RECORDS = 50;
92
+
93
+ // Read up to `SNIFF_RECORDS` records with one candidate delimiter, in
94
+ // repair mode so damage never aborts the probe, and report how consistent
95
+ // the record widths are. A real delimiter produces the same width on
96
+ // nearly every line; a wrong one produces width 1 or noise.
97
+ function probeDelimiter(sample, delimiter, options) {
98
+ const widths = [];
99
+ const machine = new CsvMachine({
100
+ ...options,
101
+ delimiter,
102
+ headers: false,
103
+ typed: false,
104
+ repair: true,
105
+ onEvent: null,
106
+ onRepair: null,
107
+ });
108
+ let rows;
109
+ try {
110
+ rows = machine.parseAll(sample);
111
+ }
112
+ catch {
113
+ return { score: 0, width: 0 };
114
+ }
115
+ const limit = rows.length < SNIFF_RECORDS ? rows.length : SNIFF_RECORDS;
116
+ for (let i = 0; i < limit; i++)
117
+ widths.push(rows[i].length);
118
+ if (widths.length === 0)
119
+ return { score: 0, width: 0 };
120
+
121
+ const counts = new Map();
122
+ for (const w of widths)
123
+ counts.set(w, (counts.get(w) ?? 0) + 1);
124
+ let modal = 0;
125
+ let best = 0;
126
+ for (const [w, n] of counts) {
127
+ if (n > best || (n === best && w > modal)) {
128
+ best = n;
129
+ modal = w;
130
+ }
131
+ }
132
+ // A single column means the delimiter never appeared: no evidence at all,
133
+ // not perfect evidence.
134
+ if (modal < 2)
135
+ return { score: 0, width: modal };
136
+ return { score: best / widths.length, width: modal, rows };
137
+ }
138
+
139
+ /**
140
+ * Guess a CSV document's dialect from its text.
141
+ *
142
+ * The delimiter is chosen by consistency: each candidate is parsed and
143
+ * scored by how often it produces the same field count, because a
144
+ * separator that is really a separator divides every record the same way
145
+ * and one that is not divides them arbitrarily. Ties go to the delimiter
146
+ * yielding more columns, since a spurious separator can only ever split
147
+ * fewer.
148
+ *
149
+ * Header detection asks whether the first record looks unlike the rest:
150
+ * if every cell in it is a non-numeric string while some column below is
151
+ * consistently numeric or date-like, that first record is naming columns
152
+ * rather than carrying data.
153
+ *
154
+ * @param {string} text - CSV source text (a prefix is enough)
155
+ * @param {object} [options] - Reader options that affect parsing (`quote`, `comment`)
156
+ * @returns {{delimiter: string, quote: string|null, headers: boolean, width: number, confidence: number}}
157
+ * The guess, with `confidence` in 0..1
158
+ * @example
159
+ * sniffCsvDialect('a;b;c\n1;2;3'); // { delimiter: ';', headers: true, ... }
160
+ */
161
+ export function sniffCsvDialect(text, options = undefined) {
162
+ const sample = text.length > SNIFF_BYTES ? text.slice(0, text.lastIndexOf('\n', SNIFF_BYTES) + 1 || SNIFF_BYTES) : text;
163
+ const opts = options ?? {};
164
+ let bestDelim = opts.delimiter && opts.delimiter !== 'auto' ? opts.delimiter : ',';
165
+ let bestScore = -1;
166
+ let bestWidth = 0;
167
+ let bestRows = null;
168
+
169
+ for (const candidate of CANDIDATES) {
170
+ const probe = probeDelimiter(sample, candidate, opts);
171
+ if (probe.score > bestScore || (probe.score === bestScore && probe.width > bestWidth)) {
172
+ bestScore = probe.score;
173
+ bestWidth = probe.width;
174
+ bestDelim = candidate;
175
+ bestRows = probe.rows ?? null;
176
+ }
177
+ }
178
+
179
+ return {
180
+ delimiter: bestDelim,
181
+ quote: opts.quote === undefined ? '"' : opts.quote,
182
+ headers: bestRows === null ? false : looksLikeHeader(bestRows),
183
+ width: bestWidth,
184
+ confidence: bestScore < 0 ? 0 : bestScore,
185
+ };
186
+ }
187
+
188
+ // The first record names columns when it is all non-empty text and at
189
+ // least one column below it is consistently non-text. A table that is
190
+ // strings all the way down gives no evidence either way, so it is not a
191
+ // header — inventing one would silently eat a data row.
192
+ function looksLikeHeader(rows) {
193
+ if (rows.length < 2)
194
+ return false;
195
+ const head = rows[0];
196
+ for (const cell of head) {
197
+ if (cell === '' || cell === null || typeof coerceCsvValue(String(cell)) !== 'string')
198
+ return false;
199
+ }
200
+ const limit = rows.length < SNIFF_RECORDS ? rows.length : SNIFF_RECORDS;
201
+ for (let col = 0; col < head.length; col++) {
202
+ let typed = 0;
203
+ let seen = 0;
204
+ for (let r = 1; r < limit; r++) {
205
+ const cell = rows[r][col];
206
+ if (cell === undefined || cell === '' || cell === null)
207
+ continue;
208
+ seen++;
209
+ if (typeof coerceCsvValue(String(cell)) !== 'string')
210
+ typed++;
211
+ }
212
+ if (seen > 0 && typed === seen)
213
+ return true;
214
+ }
215
+ return false;
216
+ }
217
+
218
+ //#endregion
219
+
220
+ //#region writing
221
+
222
+ const RE_UNSAFE_CACHE = new Map();
223
+
224
+ function needsQuoteTester(delimiter, quote) {
225
+ const key = delimiter + quote;
226
+ let test = RE_UNSAFE_CACHE.get(key);
227
+ if (test === undefined) {
228
+ const dc = delimiter.charCodeAt(0);
229
+ const qc = quote.length === 0 ? -1 : quote.charCodeAt(0);
230
+ test = (s) => {
231
+ for (let i = 0; i < s.length; i++) {
232
+ const c = s.charCodeAt(i);
233
+ if (c === dc || c === qc || c === 0x0A || c === 0x0D)
234
+ return true;
235
+ }
236
+ // leading or trailing whitespace only survives a round-trip when
237
+ // the field is quoted, because a lenient reader may trim it
238
+ if (s.length !== 0) {
239
+ const a = s.charCodeAt(0);
240
+ const b = s.charCodeAt(s.length - 1);
241
+ if (a === 0x20 || a === 0x09 || b === 0x20 || b === 0x09)
242
+ return true;
243
+ }
244
+ return false;
245
+ };
246
+ RE_UNSAFE_CACHE.set(key, test);
247
+ }
248
+ return test;
249
+ }
250
+
251
+ /**
252
+ * Render one value as CSV cell text. `null`/`undefined` become empty,
253
+ * bigints lose the `n` suffix JOSL uses (CSV has no type marks), and the
254
+ * JOSL date classes and `Date` render as ISO-8601.
255
+ * @param {*} value - The value
256
+ * @returns {string} Cell text, unquoted
257
+ */
258
+ export function formatCsvValue(value) {
259
+ if (value === null || value === undefined)
260
+ return '';
261
+ switch (typeof value) {
262
+ case 'string': return value;
263
+ case 'number': return Number.isFinite(value) ? String(value) : '';
264
+ case 'bigint': return String(value);
265
+ case 'boolean': return value ? 'true' : 'false';
266
+ default: break;
267
+ }
268
+ if (value instanceof Date)
269
+ return Number.isNaN(value.getTime()) ? '' : value.toISOString();
270
+ // LocalDate / LocalTime / LocalDateTime, and anything else that knows
271
+ // how to render itself
272
+ return String(value);
273
+ }
274
+
275
+ /**
276
+ * Serialize records to a CSV document.
277
+ *
278
+ * A field is quoted only when it has to be — when it contains the
279
+ * delimiter, a quote, a newline, or edge whitespace a lenient reader
280
+ * might trim. Everything else is written bare, which keeps the output
281
+ * both smaller and diff-friendly.
282
+ *
283
+ * @param {Array<Array|object>} rows - Records: arrays, or objects keyed by column
284
+ * @param {object} [options] - Writer options
285
+ * @param {string} [options.delimiter=','] - Field separator
286
+ * @param {string} [options.quote='"'] - Quote character
287
+ * @param {string} [options.newline='\r\n'] - Record terminator; RFC 4180 §2.1
288
+ * specifies CRLF, which is also what spreadsheet software expects
289
+ * @param {string[]} [options.fields] - Column order; inferred from the first
290
+ * object record when omitted
291
+ * @param {boolean} [options.header=true] - Emit a header row for object records
292
+ * @returns {string} The CSV document
293
+ * @example
294
+ * stringifyCsv([{ a: 1, b: 'x,y' }]); // 'a,b\r\n1,"x,y"\r\n'
295
+ */
296
+ export function stringifyCsv(rows, options = {}) {
297
+ let out = '';
298
+ for (const chunk of stringifyCsvChunks(rows, options))
299
+ out += chunk;
300
+ return out;
301
+ }
302
+
303
+ /**
304
+ * Serialize records as an iterable of chunks, one record at a time, so a
305
+ * large table never exists as a single string.
306
+ * @param {Iterable<Array|object>} rows - Records
307
+ * @param {object} [options] - Writer options; see `stringifyCsv`
308
+ * @yields {string} One record (or the header) at a time
309
+ */
310
+ export function* stringifyCsvChunks(rows, options = {}) {
311
+ const delimiter = options.delimiter ?? ',';
312
+ const quote = options.quote ?? '"';
313
+ const newline = options.newline ?? '\r\n';
314
+ const wantHeader = options.header !== false;
315
+ const unsafe = needsQuoteTester(delimiter, quote);
316
+ const escaped = quote + quote;
317
+
318
+ const cell = (value) => {
319
+ const s = formatCsvValue(value);
320
+ if (quote.length === 0 || !unsafe(s))
321
+ return s;
322
+ return quote + (s.includes(quote) ? s.replaceAll(quote, escaped) : s) + quote;
323
+ };
324
+
325
+ let fields = options.fields ?? null;
326
+ let emittedHeader = false;
327
+ for (const row of rows) {
328
+ if (Array.isArray(row)) {
329
+ let line = '';
330
+ for (let i = 0; i < row.length; i++)
331
+ line += (i === 0 ? '' : delimiter) + cell(row[i]);
332
+ yield line + newline;
333
+ continue;
334
+ }
335
+ if (fields === null)
336
+ fields = Object.keys(row);
337
+ if (wantHeader && !emittedHeader) {
338
+ emittedHeader = true;
339
+ let head = '';
340
+ for (let i = 0; i < fields.length; i++)
341
+ head += (i === 0 ? '' : delimiter) + cell(fields[i]);
342
+ yield head + newline;
343
+ }
344
+ let line = '';
345
+ for (let i = 0; i < fields.length; i++)
346
+ line += (i === 0 ? '' : delimiter) + cell(row[fields[i]]);
347
+ yield line + newline;
348
+ }
349
+ // an explicit field list still deserves its header when there were no
350
+ // records to infer one from
351
+ if (wantHeader && !emittedHeader && fields !== null && options.fields !== undefined) {
352
+ let head = '';
353
+ for (let i = 0; i < fields.length; i++)
354
+ head += (i === 0 ? '' : delimiter) + cell(fields[i]);
355
+ yield head + newline;
356
+ }
357
+ }
358
+
359
+ //#endregion
360
+
361
+ export { CsvSyntaxError, CSV_CODES, coerceCsvValue };
362
+
363
+ //#endregion
package/src/errors.js ADDED
@@ -0,0 +1,103 @@
1
+ //#region JOSL errors
2
+ // All parse errors carry a 1-based line and column plus an optional `hint`
3
+ // aimed at machine-repair loops: an LLM that produced almost-valid JOSL can
4
+ // be re-prompted with `message` + `hint` to fix its own output, mirroring
5
+ // the suggestion style of the other jaren error classes.
6
+
7
+ /**
8
+ * Shared constructor body for the line/column syntax errors. The `name`
9
+ * is passed as a string literal because the bundle is minified and a
10
+ * mangled class name must not leak into `error.name`.
11
+ */
12
+ class LineColumnSyntaxError extends SyntaxError {
13
+ /**
14
+ * @param {string} name - The public class name for `error.name`
15
+ * @param {string} message - What is wrong
16
+ * @param {number} line - 1-based physical line number
17
+ * @param {number} column - 1-based column number
18
+ * @param {string} [hint] - Repair suggestion for machine-repair loops
19
+ */
20
+ constructor(name, message, line, column, hint = undefined) {
21
+ super(`${message} at line ${line}, column ${column}${hint ? ` (${hint})` : ''}`);
22
+ this.name = name;
23
+ this.line = line;
24
+ this.column = column;
25
+ this.hint = hint;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Error thrown when JOSL / TOML source text violates the grammar or the
31
+ * table redefinition rules.
32
+ */
33
+ export class JoslSyntaxError extends LineColumnSyntaxError {
34
+ /**
35
+ * @param {string} message - What is wrong
36
+ * @param {number} line - 1-based physical line number
37
+ * @param {number} column - 1-based column number
38
+ * @param {string} [hint] - Repair suggestion for machine-repair loops
39
+ */
40
+ constructor(message, line, column, hint = undefined) {
41
+ super('JoslSyntaxError', message, line, column, hint);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Error thrown when a value cannot be represented in the requested output
47
+ * mode (e.g. `null` or a RegExp in strict TOML mode, a scalar root).
48
+ */
49
+ export class JoslStringifyError extends Error {
50
+ /**
51
+ * @param {string} message - What is wrong
52
+ * @param {(string|number)[]} [path] - Path of the offending value
53
+ */
54
+ constructor(message, path = []) {
55
+ super(path.length ? `${message} at /${path.join('/')}` : message);
56
+ this.name = 'JoslStringifyError';
57
+ this.path = path;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Error thrown when CSV source text violates RFC 4180 in strict mode.
63
+ *
64
+ * Every condition this reports is also a *repairable* one: `repair: true`
65
+ * turns each into a logged repair instead of a throw, so the same `code`
66
+ * appears either as `error.code` here or as an entry in the reader's
67
+ * repair log. Carrying the code both ways is what lets a caller move
68
+ * between the two modes without re-learning the diagnosis.
69
+ */
70
+ export class CsvSyntaxError extends SyntaxError {
71
+ /**
72
+ * @param {string} code - Stable `CSV1xxx` diagnosis code
73
+ * @param {string} message - What is wrong
74
+ * @param {number} line - 1-based physical line number
75
+ * @param {number} column - 1-based column number
76
+ * @param {string} [hint] - Repair suggestion for machine-repair loops
77
+ */
78
+ constructor(code, message, line, column, hint = undefined) {
79
+ super(`${code}: ${message} at line ${line}, column ${column}${hint ? ` (${hint})` : ''}`);
80
+ this.name = 'CsvSyntaxError';
81
+ this.code = code;
82
+ this.line = line;
83
+ this.column = column;
84
+ this.hint = hint;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Error thrown when JSONX source text violates the grammar.
90
+ */
91
+ export class JsonxSyntaxError extends LineColumnSyntaxError {
92
+ /**
93
+ * @param {string} message - What is wrong
94
+ * @param {number} line - 1-based physical line number
95
+ * @param {number} column - 1-based column number
96
+ * @param {string} [hint] - Repair suggestion for machine-repair loops
97
+ */
98
+ constructor(message, line, column, hint = undefined) {
99
+ super('JsonxSyntaxError', message, line, column, hint);
100
+ }
101
+ }
102
+
103
+ //#endregion