@tabnas/transduce 0.1.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.
- package/LICENSE +21 -0
- package/dist/capability.d.ts +6 -0
- package/dist/capability.js +52 -0
- package/dist/capability.js.map +1 -0
- package/dist/datum.d.ts +64 -0
- package/dist/datum.js +391 -0
- package/dist/datum.js.map +1 -0
- package/dist/error.d.ts +43 -0
- package/dist/error.js +169 -0
- package/dist/error.js.map +1 -0
- package/dist/event.d.ts +56 -0
- package/dist/event.js +101 -0
- package/dist/event.js.map +1 -0
- package/dist/guard.d.ts +20 -0
- package/dist/guard.js +90 -0
- package/dist/guard.js.map +1 -0
- package/dist/json.d.ts +4 -0
- package/dist/json.js +92 -0
- package/dist/json.js.map +1 -0
- package/dist/limits.d.ts +36 -0
- package/dist/limits.js +133 -0
- package/dist/limits.js.map +1 -0
- package/dist/lines.d.ts +39 -0
- package/dist/lines.js +1139 -0
- package/dist/lines.js.map +1 -0
- package/dist/matcher.d.ts +26 -0
- package/dist/matcher.js +245 -0
- package/dist/matcher.js.map +1 -0
- package/dist/parser-source.d.ts +27 -0
- package/dist/parser-source.js +194 -0
- package/dist/parser-source.js.map +1 -0
- package/dist/route.d.ts +61 -0
- package/dist/route.js +197 -0
- package/dist/route.js.map +1 -0
- package/dist/rule-events.d.ts +58 -0
- package/dist/rule-events.js +598 -0
- package/dist/rule-events.js.map +1 -0
- package/dist/scan.d.ts +19 -0
- package/dist/scan.js +70 -0
- package/dist/scan.js.map +1 -0
- package/dist/selector.d.ts +40 -0
- package/dist/selector.js +154 -0
- package/dist/selector.js.map +1 -0
- package/dist/sink.d.ts +29 -0
- package/dist/sink.js +184 -0
- package/dist/sink.js.map +1 -0
- package/dist/source.d.ts +37 -0
- package/dist/source.js +113 -0
- package/dist/source.js.map +1 -0
- package/dist/table-from-json.d.ts +12 -0
- package/dist/table-from-json.js +217 -0
- package/dist/table-from-json.js.map +1 -0
- package/dist/table.d.ts +81 -0
- package/dist/table.js +141 -0
- package/dist/table.js.map +1 -0
- package/dist/transduce.d.ts +28 -0
- package/dist/transduce.js +109 -0
- package/dist/transduce.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/value.d.ts +16 -0
- package/dist/value.js +96 -0
- package/dist/value.js.map +1 -0
- package/package.json +65 -0
package/dist/lines.js
ADDED
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Copyright (c) 2026 tabnas, MIT License */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.LinesSource = exports.LineFormat = exports.DEFAULT_CHUNK_BYTES = void 0;
|
|
5
|
+
// `LinesSource`: JSON Lines and CSV a record (or a chunk of records) at a
|
|
6
|
+
// time, from a synchronous iterable of chunks or pushed chunk by chunk.
|
|
7
|
+
//
|
|
8
|
+
// The engine parses a whole string, so a document is retained at least
|
|
9
|
+
// once however it is consumed. Line-delimited formats do not need to be one
|
|
10
|
+
// document: JSON Lines is one value per line, and CSV is a header plus
|
|
11
|
+
// independent records, so each can be parsed a piece at a time with one
|
|
12
|
+
// reused parser and the memory a run needs stops depending on the input's
|
|
13
|
+
// size. Its events are exactly the whole-input parse's (the array of the
|
|
14
|
+
// per-line values, or of the records).
|
|
15
|
+
//
|
|
16
|
+
// Both formats cut the input where the grammar ends a record, reading the
|
|
17
|
+
// text as the grammar's own lexer does (with the line characters, quotes,
|
|
18
|
+
// separators and comments its parser's resolved options set), and nothing
|
|
19
|
+
// read is left unparsed but a blank JSON Lines record, which the grammar
|
|
20
|
+
// skips too.
|
|
21
|
+
//
|
|
22
|
+
// JSON Lines: each record is parsed with one `@tabnas/json` parser. A
|
|
23
|
+
// record is the text between line characters outside a string, so a lone
|
|
24
|
+
// `\r` ends one as `\n` does. A record of nothing but the grammar's spaces
|
|
25
|
+
// (a space or a tab) is blank and skipped; any other is parsed, so a line
|
|
26
|
+
// holding a form feed or a no-break space fails as the grammar fails it.
|
|
27
|
+
// On the incremental path (`runIncremental`, or a writer asked for it) the
|
|
28
|
+
// record goes through the rule-event adapter, reset per record, so numbers
|
|
29
|
+
// keep their lexemes; on the walking path (`run`) the value is walked and
|
|
30
|
+
// numbers carry none. A record that does not parse is `INPUT_INVALID` with
|
|
31
|
+
// its line's number as the row.
|
|
32
|
+
//
|
|
33
|
+
// CSV: the input is cut into chunks of whole records. A quote opens a
|
|
34
|
+
// quoted field only where the lexer starts a token (a record's start, after
|
|
35
|
+
// the field separator or a space), so a quote inside a field is that
|
|
36
|
+
// field's text; the configured quote reads `""` as one quote; a quoted
|
|
37
|
+
// field, a backtick string and a block comment may span lines; and a line
|
|
38
|
+
// character anywhere else, a lone `\r` as much as `\n`, ends a record. The
|
|
39
|
+
// header record, the first record the grammar reads (a blank or comment
|
|
40
|
+
// line before it is none), is kept and prepended to every chunk after the
|
|
41
|
+
// first when `header` is on, so the reused `@tabnas/csv` parser names each
|
|
42
|
+
// record's fields as the whole input would. A chunk closes at the first
|
|
43
|
+
// record boundary at or past `DEFAULT_CHUNK_BYTES` (or the configured
|
|
44
|
+
// size), so one chunk, and never a fraction of a record, is what a parse
|
|
45
|
+
// holds. An input that ends inside a quoted field fails with the grammar's
|
|
46
|
+
// `unterminated_string`, in the header as anywhere. CSV is walked on both
|
|
47
|
+
// paths.
|
|
48
|
+
//
|
|
49
|
+
// The input is read a piece at a time, and a piece ends just past each of
|
|
50
|
+
// the grammar's line endings: one of its line characters (a lone `\r` or a
|
|
51
|
+
// configured separator as much as `\n`), a `\r\n`, or under `record.empty`
|
|
52
|
+
// the whole line token the lexer reads. A line character inside one of the
|
|
53
|
+
// grammar's fixed tokens (a field separator such as `"\n~"`) is the token's
|
|
54
|
+
// and ends no piece. So a record never waits for a `\n` to end, and a chunk
|
|
55
|
+
// can close after any record, however many of them one `\n`-terminated line
|
|
56
|
+
// holds. Where the input so far ends inside what may be one line ending,
|
|
57
|
+
// the piece waits for the next chunk of it.
|
|
58
|
+
//
|
|
59
|
+
// Every count is in UTF-8 bytes of the input as it arrives. A single
|
|
60
|
+
// record larger than `max_record_bytes` (a record with its line ending; for
|
|
61
|
+
// the CSV header, everything up to its end) fails with that limit's name
|
|
62
|
+
// the moment it passes the limit, whatever its length, rather than growing
|
|
63
|
+
// a chunk without bound.
|
|
64
|
+
const csv_1 = require("@tabnas/csv");
|
|
65
|
+
const json_1 = require("@tabnas/json");
|
|
66
|
+
const error_1 = require("./error");
|
|
67
|
+
const event_1 = require("./event");
|
|
68
|
+
const guard_1 = require("./guard");
|
|
69
|
+
const limits_1 = require("./limits");
|
|
70
|
+
const rule_events_1 = require("./rule-events");
|
|
71
|
+
const source_1 = require("./source");
|
|
72
|
+
// How much of the input one CSV parse holds, at most one record over.
|
|
73
|
+
exports.DEFAULT_CHUNK_BYTES = 256 * 1024;
|
|
74
|
+
exports.LineFormat = Object.freeze({
|
|
75
|
+
jsonl() {
|
|
76
|
+
return { type: 'jsonl' };
|
|
77
|
+
},
|
|
78
|
+
// CSV with a header line and the grammar's default options, or the given
|
|
79
|
+
// ones.
|
|
80
|
+
csv(header = true, options = {}) {
|
|
81
|
+
return { type: 'csv', header, options };
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
// A line-delimited input as a source.
|
|
85
|
+
class LinesSource {
|
|
86
|
+
// `input` may be `null` for a source used only through `writer`.
|
|
87
|
+
constructor(input, format) {
|
|
88
|
+
this.sourceLimits = limits_1.Limits.default();
|
|
89
|
+
this.abortFlag = new limits_1.AbortFlag();
|
|
90
|
+
this.sourceMetrics = new limits_1.Metrics();
|
|
91
|
+
this.chunkSize = exports.DEFAULT_CHUNK_BYTES;
|
|
92
|
+
this.consumed = false;
|
|
93
|
+
this.input = input;
|
|
94
|
+
this.format = format;
|
|
95
|
+
}
|
|
96
|
+
limits(limits) {
|
|
97
|
+
this.sourceLimits = limits;
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
abort(abort) {
|
|
101
|
+
this.abortFlag = abort;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
metrics(metrics) {
|
|
105
|
+
this.sourceMetrics = metrics;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
// The CSV chunk size; a chunk closes at the first record boundary at or
|
|
109
|
+
// past it. Zero closes a chunk at every record.
|
|
110
|
+
chunkBytes(bytes) {
|
|
111
|
+
this.chunkSize = bytes;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
// Both formats through the walk; JSON Lines numbers carry no lexeme.
|
|
115
|
+
run(sink) {
|
|
116
|
+
return this.drive(this.writer(sink, false));
|
|
117
|
+
}
|
|
118
|
+
// JSON Lines through the rule-event adapter (lexemes kept), CSV through
|
|
119
|
+
// the walk.
|
|
120
|
+
runIncremental(sink) {
|
|
121
|
+
return this.drive(this.writer(sink, true));
|
|
122
|
+
}
|
|
123
|
+
drive(writer) {
|
|
124
|
+
if (this.consumed) {
|
|
125
|
+
throw new error_1.Fail('STREAM_REUSED', 'a LinesSource reads its input once');
|
|
126
|
+
}
|
|
127
|
+
if (null === this.input) {
|
|
128
|
+
throw new TypeError('LinesSource: no input; use writer() to push chunks');
|
|
129
|
+
}
|
|
130
|
+
const input = this.input;
|
|
131
|
+
this.input = null;
|
|
132
|
+
this.consumed = true;
|
|
133
|
+
const chunks = 'string' === typeof input || input instanceof Uint8Array ? [input] : input;
|
|
134
|
+
for (const chunk of chunks) {
|
|
135
|
+
if ('stop' === writer.write(chunk))
|
|
136
|
+
return 'stop';
|
|
137
|
+
}
|
|
138
|
+
return writer.end();
|
|
139
|
+
}
|
|
140
|
+
// A writer that pushes the input into `sink`, on the incremental path
|
|
141
|
+
// when `incremental` is true.
|
|
142
|
+
writer(sink, incremental = false) {
|
|
143
|
+
const options = {
|
|
144
|
+
limits: this.sourceLimits,
|
|
145
|
+
abort: this.abortFlag,
|
|
146
|
+
metrics: this.sourceMetrics,
|
|
147
|
+
chunkBytes: this.chunkSize,
|
|
148
|
+
};
|
|
149
|
+
const driver = 'jsonl' === this.format.type
|
|
150
|
+
? incremental
|
|
151
|
+
? new JsonlIncremental(sink, options)
|
|
152
|
+
: new JsonlWalk(sink, options)
|
|
153
|
+
: new CsvWalk(sink, options, this.format.header, this.format.options);
|
|
154
|
+
return new Writer(driver);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.LinesSource = LinesSource;
|
|
158
|
+
// The writer's state machine: a stop or a failure ends it for good.
|
|
159
|
+
class Writer {
|
|
160
|
+
constructor(driver) {
|
|
161
|
+
this.started = false;
|
|
162
|
+
this.state = 'open';
|
|
163
|
+
this.failure = null;
|
|
164
|
+
this.driver = driver;
|
|
165
|
+
this.onPiece = (row, text, bytes) => this.driver.piece(row, text, bytes);
|
|
166
|
+
}
|
|
167
|
+
guard(step) {
|
|
168
|
+
if ('stopped' === this.state)
|
|
169
|
+
return 'stop';
|
|
170
|
+
if ('failed' === this.state)
|
|
171
|
+
throw this.failure;
|
|
172
|
+
if ('ended' === this.state)
|
|
173
|
+
throw error_1.Fail.protocol('the line source has already ended');
|
|
174
|
+
try {
|
|
175
|
+
if (!this.started) {
|
|
176
|
+
this.started = true;
|
|
177
|
+
if ('stop' === this.driver.begin())
|
|
178
|
+
return this.finish('stopped');
|
|
179
|
+
}
|
|
180
|
+
const flow = step();
|
|
181
|
+
if ('stop' === flow)
|
|
182
|
+
return this.finish('stopped');
|
|
183
|
+
return flow;
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
this.failure = err;
|
|
187
|
+
this.finish('failed');
|
|
188
|
+
throw err;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
finish(state) {
|
|
192
|
+
this.state = state;
|
|
193
|
+
this.driver.close();
|
|
194
|
+
return 'stop';
|
|
195
|
+
}
|
|
196
|
+
write(chunk) {
|
|
197
|
+
return this.guard(() => {
|
|
198
|
+
const bytes = 'string' === typeof chunk ? Buffer.from(chunk, 'utf8') : chunk;
|
|
199
|
+
return this.driver.splitter.feed(bytes, this.onPiece);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
end() {
|
|
203
|
+
const flow = this.guard(() => {
|
|
204
|
+
if ('stop' === this.driver.splitter.end(this.onPiece))
|
|
205
|
+
return 'stop';
|
|
206
|
+
return this.driver.finish();
|
|
207
|
+
});
|
|
208
|
+
if ('open' === this.state) {
|
|
209
|
+
this.finish('ended');
|
|
210
|
+
return flow;
|
|
211
|
+
}
|
|
212
|
+
return flow;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const EMPTY = new Uint8Array(0);
|
|
216
|
+
const ENCODER = new TextEncoder();
|
|
217
|
+
// The characters of one of the engine's resolved character maps while it
|
|
218
|
+
// lexes their kind, else none.
|
|
219
|
+
function lexed(on, chars) {
|
|
220
|
+
return on && chars ? Object.keys(chars) : [];
|
|
221
|
+
}
|
|
222
|
+
// The input in pieces, each ending just past a line ending: one of the
|
|
223
|
+
// grammar's line characters, a `\r\n`, or under `line.single`
|
|
224
|
+
// (`record.empty`) the whole line token the lexer reads, every line
|
|
225
|
+
// character up to a repeated one. A line character inside one of the
|
|
226
|
+
// grammar's fixed tokens (a field separator such as `"\n~"`) is the token's
|
|
227
|
+
// and ends no piece, and where a fixed token holds one, a piece takes the
|
|
228
|
+
// whole run of line characters the lexer reads as one token, so that no
|
|
229
|
+
// piece starts inside a line token. A record therefore never ends inside a
|
|
230
|
+
// piece, only at its end. Each piece carries the row the engine gives its
|
|
231
|
+
// first character: one more than the row characters before it. Where the
|
|
232
|
+
// input so far ends inside what may be a line ending, the piece waits for
|
|
233
|
+
// more.
|
|
234
|
+
class PieceSplitter {
|
|
235
|
+
constructor(line, rows, single, fixed) {
|
|
236
|
+
// The bytes of the piece being read, and any after it not yet taken.
|
|
237
|
+
this.pending = EMPTY;
|
|
238
|
+
// How much of `pending` has been searched for a line ending.
|
|
239
|
+
this.scanned = 0;
|
|
240
|
+
// Once a line ending is found: where its token ends so far, and its
|
|
241
|
+
// characters.
|
|
242
|
+
this.tokenEnd = -1;
|
|
243
|
+
this.token = [];
|
|
244
|
+
// Where the next piece starts: its row, and its byte offset in it.
|
|
245
|
+
this.row = 1;
|
|
246
|
+
this.offset = 0;
|
|
247
|
+
this.ends = new Uint8Array(256);
|
|
248
|
+
// How many more bytes the piece being read may take, and the failure
|
|
249
|
+
// when it takes more, given the row it starts on: the bound is the
|
|
250
|
+
// record's, so the format that reads the records sets both.
|
|
251
|
+
this.budget = () => Infinity;
|
|
252
|
+
this.over = (row) => error_1.Fail.input(`line ${row} is too long`).at(row, 1);
|
|
253
|
+
// The engine keeps a character outside the basic plane as its two
|
|
254
|
+
// UTF-16 halves, each a line character; the input holds it as one
|
|
255
|
+
// four-byte form, which ends a piece when both halves are line
|
|
256
|
+
// characters.
|
|
257
|
+
this.line = line.filter((c) => !isHigh(c) && !isLow(c));
|
|
258
|
+
for (const h of line.filter(isHigh)) {
|
|
259
|
+
for (const l of line.filter(isLow))
|
|
260
|
+
this.line.push(h + l);
|
|
261
|
+
}
|
|
262
|
+
this.forms = this.line.map((c) => ENCODER.encode(c));
|
|
263
|
+
for (const form of this.forms)
|
|
264
|
+
this.ends[form[form.length - 1]] = 1;
|
|
265
|
+
this.single = single;
|
|
266
|
+
this.spanning = fixed
|
|
267
|
+
.filter((token) => Array.from({ length: token.length }, (_, i) => token[i]).some((c) => line.includes(c)))
|
|
268
|
+
.map((token) => ENCODER.encode(token));
|
|
269
|
+
this.rows = rows;
|
|
270
|
+
}
|
|
271
|
+
// The row the next piece starts on.
|
|
272
|
+
get nextRow() {
|
|
273
|
+
return this.row;
|
|
274
|
+
}
|
|
275
|
+
feed(bytes, piece) {
|
|
276
|
+
if (0 === bytes.length)
|
|
277
|
+
return 'continue';
|
|
278
|
+
this.pending = 0 === this.pending.length ? bytes : Buffer.concat([this.pending, bytes]);
|
|
279
|
+
return this.drain(piece, false);
|
|
280
|
+
}
|
|
281
|
+
end(piece) {
|
|
282
|
+
if ('stop' === this.drain(piece, true))
|
|
283
|
+
return 'stop';
|
|
284
|
+
if (0 === this.pending.length)
|
|
285
|
+
return 'continue';
|
|
286
|
+
const raw = this.pending;
|
|
287
|
+
this.pending = EMPTY;
|
|
288
|
+
this.scanned = 0;
|
|
289
|
+
return this.emit(raw, piece);
|
|
290
|
+
}
|
|
291
|
+
drain(piece, atEnd) {
|
|
292
|
+
for (;;) {
|
|
293
|
+
if (this.tokenEnd < 0 && !this.findEnding(atEnd)) {
|
|
294
|
+
if (this.pending.length > this.budget())
|
|
295
|
+
throw this.over(this.row);
|
|
296
|
+
return 'continue';
|
|
297
|
+
}
|
|
298
|
+
if (!this.followToken(atEnd))
|
|
299
|
+
return 'continue';
|
|
300
|
+
const raw = this.pending.subarray(0, this.tokenEnd);
|
|
301
|
+
this.pending = this.pending.subarray(this.tokenEnd);
|
|
302
|
+
this.scanned = 0;
|
|
303
|
+
this.tokenEnd = -1;
|
|
304
|
+
this.token = [];
|
|
305
|
+
if ('stop' === this.emit(raw, piece))
|
|
306
|
+
return 'stop';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Finds the first line ending past `scanned`, checked against the line
|
|
310
|
+
// character's whole UTF-8 form, and starts its token. False while the
|
|
311
|
+
// input so far holds none, or cannot yet tell where one ends.
|
|
312
|
+
findEnding(atEnd) {
|
|
313
|
+
const p = this.pending;
|
|
314
|
+
for (let i = this.scanned; i < p.length; i++) {
|
|
315
|
+
if (!this.ends[p[i]])
|
|
316
|
+
continue;
|
|
317
|
+
const k = this.formEnding(i + 1);
|
|
318
|
+
if (k < 0)
|
|
319
|
+
continue;
|
|
320
|
+
const at = i + 1 - this.forms[k].length;
|
|
321
|
+
const inside = this.inFixed(at, k, atEnd);
|
|
322
|
+
if (true === inside)
|
|
323
|
+
continue;
|
|
324
|
+
if (null === inside) {
|
|
325
|
+
this.scanned = at;
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
this.tokenEnd = i + 1;
|
|
329
|
+
this.token = [this.line[k]];
|
|
330
|
+
if (this.tokenEnd > this.budget())
|
|
331
|
+
throw this.over(this.row);
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
this.scanned = p.length;
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
// Whether the `k`th line character, at `at`, lies inside one of the fixed
|
|
338
|
+
// tokens that hold a line character; null while the input so far cannot
|
|
339
|
+
// tell.
|
|
340
|
+
inFixed(at, k, atEnd) {
|
|
341
|
+
const p = this.pending;
|
|
342
|
+
const form = this.forms[k];
|
|
343
|
+
let unsure = false;
|
|
344
|
+
for (const token of this.spanning) {
|
|
345
|
+
for (let j = 0; j + form.length <= token.length; j++) {
|
|
346
|
+
if (!sameBytes(token, j, form, 0, form.length))
|
|
347
|
+
continue;
|
|
348
|
+
const start = at - j;
|
|
349
|
+
if (start < 0 || !sameBytes(p, start, token, 0, j))
|
|
350
|
+
continue;
|
|
351
|
+
const after = at + form.length;
|
|
352
|
+
const rest = token.length - j - form.length;
|
|
353
|
+
const have = Math.min(rest, p.length - after);
|
|
354
|
+
if (!sameBytes(p, after, token, j + form.length, have))
|
|
355
|
+
continue;
|
|
356
|
+
if (have === rest)
|
|
357
|
+
return true;
|
|
358
|
+
if (!atEnd)
|
|
359
|
+
unsure = true;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return unsure ? null : false;
|
|
363
|
+
}
|
|
364
|
+
// The line character whose UTF-8 form `pending` holds just before `end`,
|
|
365
|
+
// by its index, or -1.
|
|
366
|
+
formEnding(end) {
|
|
367
|
+
const p = this.pending;
|
|
368
|
+
next: for (let k = 0; k < this.forms.length; k++) {
|
|
369
|
+
const form = this.forms[k];
|
|
370
|
+
if (end < form.length)
|
|
371
|
+
continue;
|
|
372
|
+
for (let j = 0; j < form.length; j++) {
|
|
373
|
+
if (p[end - form.length + j] !== form[j])
|
|
374
|
+
continue next;
|
|
375
|
+
}
|
|
376
|
+
return k;
|
|
377
|
+
}
|
|
378
|
+
return -1;
|
|
379
|
+
}
|
|
380
|
+
// Takes the rest of the line token the piece ends with: under
|
|
381
|
+
// `line.single`, every line character not yet in it, as the lexer reads
|
|
382
|
+
// one; where a fixed token holds a line character, every line character,
|
|
383
|
+
// as the lexer reads a run; otherwise a `\n` after a `\r`, so that `\r\n`
|
|
384
|
+
// is one line ending and a record is held to its own line. False while
|
|
385
|
+
// the input so far cannot tell whether the token goes on.
|
|
386
|
+
followToken(atEnd) {
|
|
387
|
+
const p = this.pending;
|
|
388
|
+
for (;;) {
|
|
389
|
+
let next = -1;
|
|
390
|
+
let partial = false;
|
|
391
|
+
for (let k = 0; k < this.line.length && next < 0; k++) {
|
|
392
|
+
const c = this.line[k];
|
|
393
|
+
const goesOn = this.single
|
|
394
|
+
? !this.token.includes(c)
|
|
395
|
+
: 0 < this.spanning.length || (1 === this.token.length && '\r' === this.token[0] && '\n' === c);
|
|
396
|
+
if (!goesOn)
|
|
397
|
+
continue;
|
|
398
|
+
const form = this.forms[k];
|
|
399
|
+
const have = p.length - this.tokenEnd;
|
|
400
|
+
let prefix = true;
|
|
401
|
+
for (let j = 0; j < Math.min(have, form.length); j++) {
|
|
402
|
+
if (p[this.tokenEnd + j] !== form[j]) {
|
|
403
|
+
prefix = false;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (!prefix)
|
|
408
|
+
continue;
|
|
409
|
+
if (have >= form.length)
|
|
410
|
+
next = k;
|
|
411
|
+
else
|
|
412
|
+
partial = true;
|
|
413
|
+
}
|
|
414
|
+
if (0 <= next) {
|
|
415
|
+
this.tokenEnd += this.forms[next].length;
|
|
416
|
+
this.token.push(this.line[next]);
|
|
417
|
+
if (this.tokenEnd > this.budget())
|
|
418
|
+
throw this.over(this.row);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
return !partial || atEnd;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
emit(raw, piece) {
|
|
425
|
+
const row = this.row;
|
|
426
|
+
const bad = utf8Invalid(raw);
|
|
427
|
+
if (0 <= bad) {
|
|
428
|
+
const valid = DECODER.decode(raw.subarray(0, bad));
|
|
429
|
+
const [badRow, offset] = advance(this.rows, this.row, this.offset, valid);
|
|
430
|
+
throw error_1.Fail.input(`line ${badRow} is not UTF-8 from its byte ${offset + 1}`).at(badRow, offset + 1);
|
|
431
|
+
}
|
|
432
|
+
const text = DECODER.decode(raw);
|
|
433
|
+
[this.row, this.offset] = advance(this.rows, this.row, this.offset, text);
|
|
434
|
+
return piece(row, text, raw.length);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// The row, and the byte offset in it, just after `text`, which starts at
|
|
438
|
+
// `row` and `offset`: a row character starts the next row. The engine reads
|
|
439
|
+
// a UTF-16 code unit at a time, so a row character outside the basic plane
|
|
440
|
+
// is its two halves, and counts as both.
|
|
441
|
+
function advance(rows, row, offset, text) {
|
|
442
|
+
for (let i = 0; i < text.length; i++) {
|
|
443
|
+
const c = text[i];
|
|
444
|
+
if (rows.includes(c)) {
|
|
445
|
+
row++;
|
|
446
|
+
offset = 0;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
// A character outside the basic plane is four bytes, counted at its
|
|
450
|
+
// first half.
|
|
451
|
+
const u = c.charCodeAt(0);
|
|
452
|
+
offset += u < 0x80 ? 1 : u < 0x800 ? 2 : isHigh(c) ? 4 : isLow(c) ? 0 : 3;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return [row, offset];
|
|
456
|
+
}
|
|
457
|
+
// Whether one UTF-16 code unit is the first or the second half of a
|
|
458
|
+
// character outside the basic plane.
|
|
459
|
+
function isHigh(c) {
|
|
460
|
+
const u = c.charCodeAt(0);
|
|
461
|
+
return 1 === c.length && 0xd800 <= u && u < 0xdc00;
|
|
462
|
+
}
|
|
463
|
+
function isLow(c) {
|
|
464
|
+
const u = c.charCodeAt(0);
|
|
465
|
+
return 1 === c.length && 0xdc00 <= u && u < 0xe000;
|
|
466
|
+
}
|
|
467
|
+
// Whether `n` bytes of `a` from `ai` are those of `b` from `bi`.
|
|
468
|
+
function sameBytes(a, ai, b, bi, n) {
|
|
469
|
+
if (ai + n > a.length || bi + n > b.length)
|
|
470
|
+
return false;
|
|
471
|
+
for (let j = 0; j < n; j++)
|
|
472
|
+
if (a[ai + j] !== b[bi + j])
|
|
473
|
+
return false;
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
// The sources of a parser's fixed tokens, while it lexes them.
|
|
477
|
+
function fixedTokens(config) {
|
|
478
|
+
return config.fixed.lex
|
|
479
|
+
? Object.values(config.fixed.token)
|
|
480
|
+
.map((tin) => config.fixed.ref[tin])
|
|
481
|
+
.filter((src) => 'string' === typeof src && 0 < src.length)
|
|
482
|
+
: [];
|
|
483
|
+
}
|
|
484
|
+
const DECODER = new TextDecoder('utf-8', { ignoreBOM: true });
|
|
485
|
+
// The index of the first byte of the first invalid UTF-8 sequence, or -1.
|
|
486
|
+
function utf8Invalid(b) {
|
|
487
|
+
let i = 0;
|
|
488
|
+
const n = b.length;
|
|
489
|
+
while (i < n) {
|
|
490
|
+
const c = b[i];
|
|
491
|
+
if (c < 0x80) {
|
|
492
|
+
i++;
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
let need;
|
|
496
|
+
let min;
|
|
497
|
+
if (c >= 0xc2 && c <= 0xdf) {
|
|
498
|
+
need = 1;
|
|
499
|
+
min = 0x80;
|
|
500
|
+
}
|
|
501
|
+
else if (c >= 0xe0 && c <= 0xef) {
|
|
502
|
+
need = 2;
|
|
503
|
+
min = 0x800;
|
|
504
|
+
}
|
|
505
|
+
else if (c >= 0xf0 && c <= 0xf4) {
|
|
506
|
+
need = 3;
|
|
507
|
+
min = 0x10000;
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
return i;
|
|
511
|
+
}
|
|
512
|
+
if (i + need >= n)
|
|
513
|
+
return i;
|
|
514
|
+
let cp = c & (0xff >> (need + 2));
|
|
515
|
+
for (let k = 1; k <= need; k++) {
|
|
516
|
+
const d = b[i + k];
|
|
517
|
+
if (undefined === d || (d & 0xc0) !== 0x80)
|
|
518
|
+
return i;
|
|
519
|
+
cp = (cp << 6) | (d & 0x3f);
|
|
520
|
+
}
|
|
521
|
+
if (cp < min || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))
|
|
522
|
+
return i;
|
|
523
|
+
i += need + 1;
|
|
524
|
+
}
|
|
525
|
+
return -1;
|
|
526
|
+
}
|
|
527
|
+
// An engine error in the record or chunk that starts on row `start`, at
|
|
528
|
+
// row `line` of the input. An abort names `start` and no column: it lands
|
|
529
|
+
// between two of the engine's steps, where the engine often holds no token
|
|
530
|
+
// to place it at and falls back to 1:1 of the text it was parsing, so its
|
|
531
|
+
// own position says nothing.
|
|
532
|
+
function lineFailure(error, line, start, abort) {
|
|
533
|
+
const fail = (0, source_1.engineFailure)(error, abort);
|
|
534
|
+
if ('ABORTED' === fail.code) {
|
|
535
|
+
fail.row = start;
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
fail.row = line;
|
|
539
|
+
fail.col = (0, error_1.enginePosition)(error)?.col ?? 0;
|
|
540
|
+
}
|
|
541
|
+
return fail;
|
|
542
|
+
}
|
|
543
|
+
// A failure while the record or chunk that starts on row `start` was read:
|
|
544
|
+
// an abort that names no row names that one.
|
|
545
|
+
function atRecord(error, start) {
|
|
546
|
+
if (error instanceof error_1.Fail && 'ABORTED' === error.code && undefined === error.row) {
|
|
547
|
+
error.row = start;
|
|
548
|
+
}
|
|
549
|
+
return error;
|
|
550
|
+
}
|
|
551
|
+
// JSON Lines records from the input's pieces. A record ends at a line
|
|
552
|
+
// character outside a string, as the grammar's lexer ends one: a line
|
|
553
|
+
// character inside a string is the string's, which the grammar refuses
|
|
554
|
+
// there, so the record goes on into the next piece and fails whole as the
|
|
555
|
+
// grammar fails it. A record of nothing but the grammar's spaces is blank
|
|
556
|
+
// and skipped.
|
|
557
|
+
class JsonRecords {
|
|
558
|
+
constructor(parser, maxBytes) {
|
|
559
|
+
// The record being read: its text and bytes so far, the row it starts on
|
|
560
|
+
// (0 before its first piece), and whether a string is open in it.
|
|
561
|
+
this.text = '';
|
|
562
|
+
this.bytes = 0;
|
|
563
|
+
this.start = 0;
|
|
564
|
+
this.quote = null;
|
|
565
|
+
this.escaped = false;
|
|
566
|
+
const config = parser.internal().config;
|
|
567
|
+
this.line = lexed(config.line.lex, config.line.chars);
|
|
568
|
+
this.rows = Object.keys(config.line.rowChars ?? {});
|
|
569
|
+
this.single = !!config.line.single;
|
|
570
|
+
this.fixed = fixedTokens(config);
|
|
571
|
+
this.space = lexed(config.space.lex, config.space.chars);
|
|
572
|
+
this.quotes = lexed(config.string.lex, config.string.quoteMap);
|
|
573
|
+
this.escape = config.string.escChar ?? '\\';
|
|
574
|
+
this.maxBytes = maxBytes;
|
|
575
|
+
}
|
|
576
|
+
// A splitter for this grammar's line endings, bounded by the record.
|
|
577
|
+
splitter() {
|
|
578
|
+
const splitter = new PieceSplitter(this.line, this.rows, this.single, this.fixed);
|
|
579
|
+
splitter.budget = () => this.maxBytes - this.bytes;
|
|
580
|
+
splitter.over = (row) => {
|
|
581
|
+
const first = this.start || row;
|
|
582
|
+
return error_1.Fail.limit('max_record_bytes', this.maxBytes, `line ${first} is longer than ${this.maxBytes} bytes`).at(first, 1);
|
|
583
|
+
};
|
|
584
|
+
return splitter;
|
|
585
|
+
}
|
|
586
|
+
// Reads one piece, and hands `record` each record it ends that is not
|
|
587
|
+
// blank, with the row it starts on.
|
|
588
|
+
piece(row, text, bytes, record) {
|
|
589
|
+
if (0 === this.start)
|
|
590
|
+
this.start = row;
|
|
591
|
+
let end = -1;
|
|
592
|
+
for (let i = 0; i < text.length; i++) {
|
|
593
|
+
const c = text[i];
|
|
594
|
+
if (null !== this.quote) {
|
|
595
|
+
if (this.escaped)
|
|
596
|
+
this.escaped = false;
|
|
597
|
+
else if (c === this.escape)
|
|
598
|
+
this.escaped = true;
|
|
599
|
+
else if (c === this.quote)
|
|
600
|
+
this.quote = null;
|
|
601
|
+
}
|
|
602
|
+
else if (this.quotes.includes(c)) {
|
|
603
|
+
this.quote = c;
|
|
604
|
+
}
|
|
605
|
+
else if (this.line.includes(c)) {
|
|
606
|
+
end = i;
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (end < 0) {
|
|
611
|
+
this.text += text;
|
|
612
|
+
this.bytes += bytes;
|
|
613
|
+
return 'continue';
|
|
614
|
+
}
|
|
615
|
+
return this.take(this.text + text.slice(0, end), record);
|
|
616
|
+
}
|
|
617
|
+
// The record the input ends inside, if any.
|
|
618
|
+
end(record) {
|
|
619
|
+
if (0 === this.start)
|
|
620
|
+
return 'continue';
|
|
621
|
+
return this.take(this.text, record);
|
|
622
|
+
}
|
|
623
|
+
take(text, record) {
|
|
624
|
+
const start = this.start;
|
|
625
|
+
this.text = '';
|
|
626
|
+
this.bytes = 0;
|
|
627
|
+
this.start = 0;
|
|
628
|
+
this.quote = null;
|
|
629
|
+
this.escaped = false;
|
|
630
|
+
for (const c of text) {
|
|
631
|
+
if (!this.space.includes(c))
|
|
632
|
+
return record(start, text);
|
|
633
|
+
}
|
|
634
|
+
return 'continue';
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
// JSON Lines through the walk.
|
|
638
|
+
class JsonlWalk {
|
|
639
|
+
constructor(sink, options) {
|
|
640
|
+
this.guarded = new guard_1.Guarded(sink, options.limits, options.abort, options.metrics);
|
|
641
|
+
this.abort = options.abort;
|
|
642
|
+
this.parser = (0, json_1.make)();
|
|
643
|
+
const abort = options.abort;
|
|
644
|
+
(0, rule_events_1.prepare)(this.parser, () => !abort.isAborted());
|
|
645
|
+
this.records = new JsonRecords(this.parser, options.limits.max_record_bytes);
|
|
646
|
+
this.splitter = this.records.splitter();
|
|
647
|
+
this.onRecord = (row, text) => this.record(row, text);
|
|
648
|
+
}
|
|
649
|
+
begin() {
|
|
650
|
+
return this.guarded.event(event_1.Ev.arrayStart);
|
|
651
|
+
}
|
|
652
|
+
piece(row, text, bytes) {
|
|
653
|
+
return this.records.piece(row, text, bytes, this.onRecord);
|
|
654
|
+
}
|
|
655
|
+
record(number, text) {
|
|
656
|
+
let value;
|
|
657
|
+
try {
|
|
658
|
+
value = this.parser.parse(text);
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
throw lineFailure(err, number, number, this.abort);
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
return (0, source_1.walkValue)(value, this.guarded);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
throw atRecord(err, number);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
finish() {
|
|
671
|
+
if ('stop' === this.records.end(this.onRecord))
|
|
672
|
+
return 'stop';
|
|
673
|
+
if ('stop' === this.guarded.event(event_1.Ev.arrayEnd))
|
|
674
|
+
return 'stop';
|
|
675
|
+
return this.guarded.event(event_1.Ev.end);
|
|
676
|
+
}
|
|
677
|
+
close() {
|
|
678
|
+
this.guarded.flush();
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// JSON Lines through the rule-event adapter: one parser, one subscriber,
|
|
682
|
+
// the adapter reset before each record.
|
|
683
|
+
class JsonlIncremental {
|
|
684
|
+
constructor(sink, options) {
|
|
685
|
+
// The record being read, which an abort the adapter raised names.
|
|
686
|
+
this.reading = 0;
|
|
687
|
+
const stop = new limits_1.AbortFlag();
|
|
688
|
+
const abort = options.abort;
|
|
689
|
+
this.abort = abort;
|
|
690
|
+
const adapter = new rule_events_1.Adapter(sink, options.limits, abort, options.metrics, source_1.Prune.never(), stop);
|
|
691
|
+
this.adapter = adapter;
|
|
692
|
+
this.parser = (0, json_1.make)();
|
|
693
|
+
(0, rule_events_1.prepare)(this.parser, () => !abort.isAborted() && !stop.isAborted(), (rule, done) => adapter.onDone(rule, done));
|
|
694
|
+
this.records = new JsonRecords(this.parser, options.limits.max_record_bytes);
|
|
695
|
+
this.splitter = this.records.splitter();
|
|
696
|
+
this.onRecord = (row, text) => this.record(row, text);
|
|
697
|
+
}
|
|
698
|
+
// The adapter's own outcome, when it stopped or failed inside a parse.
|
|
699
|
+
status() {
|
|
700
|
+
const status = this.adapter.status;
|
|
701
|
+
if ('failed' === status.type) {
|
|
702
|
+
throw 0 < this.reading ? atRecord(status.error, this.reading) : status.error;
|
|
703
|
+
}
|
|
704
|
+
if ('stopped' === status.type)
|
|
705
|
+
return 'stop';
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
begin() {
|
|
709
|
+
return this.adapter.send(event_1.Ev.arrayStart);
|
|
710
|
+
}
|
|
711
|
+
piece(row, text, bytes) {
|
|
712
|
+
return this.records.piece(row, text, bytes, this.onRecord);
|
|
713
|
+
}
|
|
714
|
+
record(number, text) {
|
|
715
|
+
this.reading = number;
|
|
716
|
+
let parsed;
|
|
717
|
+
try {
|
|
718
|
+
parsed = { ok: true, value: this.parser.parse(text) };
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
parsed = { ok: false, error };
|
|
722
|
+
}
|
|
723
|
+
const status = this.status();
|
|
724
|
+
if (null !== status)
|
|
725
|
+
return status;
|
|
726
|
+
const adapter = this.adapter;
|
|
727
|
+
if (!parsed.ok)
|
|
728
|
+
throw lineFailure(parsed.error, number, number, this.abort);
|
|
729
|
+
if (adapter.complete()) {
|
|
730
|
+
adapter.reset();
|
|
731
|
+
return 'continue';
|
|
732
|
+
}
|
|
733
|
+
if (adapter.idle()) {
|
|
734
|
+
let flow;
|
|
735
|
+
try {
|
|
736
|
+
flow = adapter.walkWhole(parsed.value);
|
|
737
|
+
}
|
|
738
|
+
catch (err) {
|
|
739
|
+
throw atRecord(err, number);
|
|
740
|
+
}
|
|
741
|
+
if ('stop' === flow)
|
|
742
|
+
return 'stop';
|
|
743
|
+
adapter.reset();
|
|
744
|
+
return 'continue';
|
|
745
|
+
}
|
|
746
|
+
throw (0, rule_events_1.notStreamable)();
|
|
747
|
+
}
|
|
748
|
+
finish() {
|
|
749
|
+
if ('stop' === this.records.end(this.onRecord))
|
|
750
|
+
return 'stop';
|
|
751
|
+
const status = this.status();
|
|
752
|
+
if (null !== status)
|
|
753
|
+
return status;
|
|
754
|
+
if ('stop' === this.adapter.send(event_1.Ev.arrayEnd))
|
|
755
|
+
return 'stop';
|
|
756
|
+
return this.adapter.send(event_1.Ev.end);
|
|
757
|
+
}
|
|
758
|
+
close() {
|
|
759
|
+
this.adapter.sink.flush();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// What decides where the CSV grammar ends a record, read from its parser's
|
|
763
|
+
// resolved options and from the options the plugin builds its quote
|
|
764
|
+
// matcher from, so a separator, quote, record separator or comment setting
|
|
765
|
+
// moves the chunker's cut as it moves the grammar. Each set is empty while
|
|
766
|
+
// the engine does not lex its kind.
|
|
767
|
+
class Lexis {
|
|
768
|
+
constructor() {
|
|
769
|
+
// Line characters: outside a token, each ends a record.
|
|
770
|
+
this.line = [];
|
|
771
|
+
// Whether a run of line characters stops at a repeated one
|
|
772
|
+
// (`record.empty`), so that `\n\n` is two line tokens.
|
|
773
|
+
this.single = false;
|
|
774
|
+
this.rows = [];
|
|
775
|
+
this.space = [];
|
|
776
|
+
// The fixed tokens: the field separator, and outside strict mode the
|
|
777
|
+
// JSON structure characters.
|
|
778
|
+
this.fixed = [];
|
|
779
|
+
// The RFC 4180 quote, while the grammar's own matcher reads it: a
|
|
780
|
+
// doubled one inside is one quote, and a line character is text.
|
|
781
|
+
this.quote = null;
|
|
782
|
+
// The engine's own string quotes, after that one: an `escape` takes the
|
|
783
|
+
// next character whatever it is, and a line character is text only
|
|
784
|
+
// inside the `multi` ones (a backtick).
|
|
785
|
+
this.strings = [];
|
|
786
|
+
this.multi = [];
|
|
787
|
+
this.escape = '\\';
|
|
788
|
+
// The comment markers, longest first, each with its end: `null` for a
|
|
789
|
+
// line comment, which a line character ends without being part of.
|
|
790
|
+
this.comments = [];
|
|
791
|
+
// What ends a run of text, as characters and as token starts.
|
|
792
|
+
this.stops = [];
|
|
793
|
+
this.stopPrefixes = [];
|
|
794
|
+
// Whether the grammar ignores a space (outside strict mode) and a
|
|
795
|
+
// comment, so that a record of nothing else is blank.
|
|
796
|
+
this.spaceIgnored = false;
|
|
797
|
+
this.commentIgnored = false;
|
|
798
|
+
}
|
|
799
|
+
static csv(parser) {
|
|
800
|
+
const config = parser.internal().config;
|
|
801
|
+
const options = parser.options.plugin?.csv ?? {};
|
|
802
|
+
const lexis = new Lexis();
|
|
803
|
+
lexis.line = lexed(config.line.lex, config.line.chars);
|
|
804
|
+
lexis.single = !!config.line.single;
|
|
805
|
+
lexis.rows = Object.keys(config.line.rowChars ?? {});
|
|
806
|
+
lexis.space = lexed(config.space.lex, config.space.chars);
|
|
807
|
+
lexis.fixed = fixedTokens(config);
|
|
808
|
+
// The longest marker first, and a tie by name: the engine's order.
|
|
809
|
+
lexis.comments = config.comment.lex
|
|
810
|
+
? Object.entries(config.comment.def)
|
|
811
|
+
.filter(([, def]) => def.lex && def.start)
|
|
812
|
+
.sort(([an, a], [bn, b]) => b.start.length - a.start.length || (an < bn ? -1 : an > bn ? 1 : 0))
|
|
813
|
+
.map(([, def]) => [def.start, def.line ? null : (def.end ?? '')])
|
|
814
|
+
: [];
|
|
815
|
+
// The RFC 4180 matcher as the plugin installs it: on in strict mode
|
|
816
|
+
// unless `string.csv` is false, off otherwise unless it is true, and
|
|
817
|
+
// inert for a quote that is not one UTF-16 code unit.
|
|
818
|
+
const matcher = options.strict ? false !== options.string?.csv : true === options.string?.csv;
|
|
819
|
+
const quote = options.string?.quote;
|
|
820
|
+
lexis.quote = matcher && 'string' === typeof quote && 1 === quote.length ? quote : null;
|
|
821
|
+
lexis.strings = lexed(config.string.lex, config.string.quoteMap);
|
|
822
|
+
lexis.multi = Object.keys(config.string.multiChars ?? {});
|
|
823
|
+
lexis.escape = config.string.escChar ?? '\\';
|
|
824
|
+
// What the engine's text matcher stops at: its ender pattern, which is
|
|
825
|
+
// the space and line characters, the fixed tokens, the comment markers
|
|
826
|
+
// and the grammar's own enders.
|
|
827
|
+
lexis.stops = [...lexis.space, ...lexis.line];
|
|
828
|
+
const enders = parser.options.ender;
|
|
829
|
+
lexis.stopPrefixes = [
|
|
830
|
+
...lexis.fixed,
|
|
831
|
+
...lexis.comments.map(([start]) => start),
|
|
832
|
+
...('string' === typeof enders ? [...enders] : Array.isArray(enders) ? enders : []).filter((e) => 'string' === typeof e && 0 < e.length),
|
|
833
|
+
];
|
|
834
|
+
const ignored = config.tokenSetTins?.IGNORE ?? {};
|
|
835
|
+
lexis.spaceIgnored = !!ignored[parser.token('#SP')];
|
|
836
|
+
lexis.commentIgnored = !!ignored[parser.token('#CM')];
|
|
837
|
+
return lexis;
|
|
838
|
+
}
|
|
839
|
+
// The length of the longest fixed token at `i`, or 0.
|
|
840
|
+
fixedAt(text, i) {
|
|
841
|
+
let len = 0;
|
|
842
|
+
for (const source of this.fixed) {
|
|
843
|
+
if (source.length > len && text.startsWith(source, i))
|
|
844
|
+
len = source.length;
|
|
845
|
+
}
|
|
846
|
+
return len;
|
|
847
|
+
}
|
|
848
|
+
// The comment that starts at `i`, by its index, or -1.
|
|
849
|
+
commentAt(text, i) {
|
|
850
|
+
return this.comments.findIndex(([start]) => text.startsWith(start, i));
|
|
851
|
+
}
|
|
852
|
+
// The length of the line token at `i`.
|
|
853
|
+
lineRun(text, i) {
|
|
854
|
+
let j = i;
|
|
855
|
+
while (j < text.length) {
|
|
856
|
+
const c = text[j];
|
|
857
|
+
if (!this.line.includes(c) || (this.single && text.slice(i, j).includes(c)))
|
|
858
|
+
break;
|
|
859
|
+
j++;
|
|
860
|
+
}
|
|
861
|
+
return j - i;
|
|
862
|
+
}
|
|
863
|
+
// Whether a run of text stops at `i`.
|
|
864
|
+
endsText(text, i) {
|
|
865
|
+
return this.stops.includes(text[i]) || this.stopPrefixes.some((p) => text.startsWith(p, i));
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
const START = { type: 'start' };
|
|
869
|
+
const TEXT = { type: 'text' };
|
|
870
|
+
const QUOTED = { type: 'quoted' };
|
|
871
|
+
// Follows CSV text through the grammar's tokens, a piece at a time, to
|
|
872
|
+
// find where its records end.
|
|
873
|
+
class Scanner {
|
|
874
|
+
constructor(lexis) {
|
|
875
|
+
this.at = START;
|
|
876
|
+
// Whether the record so far holds anything the grammar does not ignore,
|
|
877
|
+
// so that it is not blank.
|
|
878
|
+
this.content = false;
|
|
879
|
+
this.lexis = lexis;
|
|
880
|
+
}
|
|
881
|
+
// Scan one piece of the input, which ends just past a line ending unless
|
|
882
|
+
// it is the last, calling `end(offset, content)` where each record in it
|
|
883
|
+
// ends: `offset` is just past the line token that ends it, and `content`
|
|
884
|
+
// says whether it held anything the grammar does not ignore. Returns
|
|
885
|
+
// whether the piece ends a record, so that a chunk may close after it.
|
|
886
|
+
piece(text, end) {
|
|
887
|
+
const lexis = this.lexis;
|
|
888
|
+
let ended = false;
|
|
889
|
+
let i = 0;
|
|
890
|
+
while (i < text.length) {
|
|
891
|
+
const c = text[i];
|
|
892
|
+
ended = false;
|
|
893
|
+
const at = this.at;
|
|
894
|
+
switch (at.type) {
|
|
895
|
+
// In the lexer's order: the RFC 4180 matcher, fixed tokens, space,
|
|
896
|
+
// lines, strings, comments, and then a run of text.
|
|
897
|
+
case 'start': {
|
|
898
|
+
let len = 0;
|
|
899
|
+
if (lexis.quote === c) {
|
|
900
|
+
this.at = QUOTED;
|
|
901
|
+
this.content = true;
|
|
902
|
+
i++;
|
|
903
|
+
}
|
|
904
|
+
else if (0 < (len = lexis.fixedAt(text, i))) {
|
|
905
|
+
this.content = true;
|
|
906
|
+
i += len;
|
|
907
|
+
}
|
|
908
|
+
else if (lexis.space.includes(c)) {
|
|
909
|
+
this.content ||= !lexis.spaceIgnored;
|
|
910
|
+
i++;
|
|
911
|
+
}
|
|
912
|
+
else if (lexis.line.includes(c)) {
|
|
913
|
+
i += lexis.lineRun(text, i);
|
|
914
|
+
end(i, this.content);
|
|
915
|
+
this.content = false;
|
|
916
|
+
ended = true;
|
|
917
|
+
}
|
|
918
|
+
else if (lexis.strings.includes(c)) {
|
|
919
|
+
this.at = { type: 'str', quote: c, multi: lexis.multi.includes(c) };
|
|
920
|
+
this.content = true;
|
|
921
|
+
i++;
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
const comment = lexis.commentAt(text, i);
|
|
925
|
+
if (0 <= comment) {
|
|
926
|
+
this.at = { type: 'comment', index: comment };
|
|
927
|
+
this.content ||= !lexis.commentIgnored;
|
|
928
|
+
i += lexis.comments[comment][0].length;
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
// A character no token starts with (an ender) is one the
|
|
932
|
+
// grammar refuses, and lexing resumes after it.
|
|
933
|
+
if (!lexis.endsText(text, i))
|
|
934
|
+
this.at = TEXT;
|
|
935
|
+
this.content = true;
|
|
936
|
+
i++;
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
case 'text':
|
|
942
|
+
if (lexis.endsText(text, i))
|
|
943
|
+
this.at = START;
|
|
944
|
+
else
|
|
945
|
+
i++;
|
|
946
|
+
break;
|
|
947
|
+
case 'quoted':
|
|
948
|
+
i++;
|
|
949
|
+
if (lexis.quote === c) {
|
|
950
|
+
if (text[i] === c)
|
|
951
|
+
i++;
|
|
952
|
+
else
|
|
953
|
+
this.at = START;
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
case 'str':
|
|
957
|
+
if (c === at.quote) {
|
|
958
|
+
this.at = START;
|
|
959
|
+
i++;
|
|
960
|
+
}
|
|
961
|
+
else if (c === lexis.escape) {
|
|
962
|
+
i += i + 1 < text.length ? 2 : 1;
|
|
963
|
+
}
|
|
964
|
+
else if (!at.multi && lexis.line.includes(c)) {
|
|
965
|
+
// The grammar refuses the string here, so the line character
|
|
966
|
+
// is read as a line.
|
|
967
|
+
this.at = START;
|
|
968
|
+
}
|
|
969
|
+
else {
|
|
970
|
+
i++;
|
|
971
|
+
}
|
|
972
|
+
break;
|
|
973
|
+
case 'comment': {
|
|
974
|
+
const close = lexis.comments[at.index][1];
|
|
975
|
+
if (null !== close && '' !== close && text.startsWith(close, i)) {
|
|
976
|
+
this.at = START;
|
|
977
|
+
i += close.length;
|
|
978
|
+
}
|
|
979
|
+
else if (null === close && lexis.line.includes(c)) {
|
|
980
|
+
this.at = START;
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
i++;
|
|
984
|
+
}
|
|
985
|
+
break;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return ended;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
// The UTF-8 length of a string.
|
|
993
|
+
function utf8Bytes(text) {
|
|
994
|
+
return Buffer.byteLength(text, 'utf8');
|
|
995
|
+
}
|
|
996
|
+
// CSV through the walk: pieces into whole records, records into chunks,
|
|
997
|
+
// each chunk parsed with one reused parser.
|
|
998
|
+
class CsvWalk {
|
|
999
|
+
constructor(sink, options, header, grammar) {
|
|
1000
|
+
// The header record as the grammar reads it, once read, and its bytes:
|
|
1001
|
+
// every chunk after the first starts with it.
|
|
1002
|
+
this.header = null;
|
|
1003
|
+
this.headerBytes = 0;
|
|
1004
|
+
this.started = false;
|
|
1005
|
+
// The chunk being filled, while one is: its text and bytes; where it may
|
|
1006
|
+
// first close (past the header in front of it, or in the first chunk past
|
|
1007
|
+
// the header record itself), as an index and in bytes; the file row its
|
|
1008
|
+
// own text starts on, and the rows of the header in front of it; where
|
|
1009
|
+
// the text it cannot close inside starts (one record, or before the
|
|
1010
|
+
// header everything up to its end), in bytes and as a row; and where the
|
|
1011
|
+
// last record ended, which is where the next one starts.
|
|
1012
|
+
this.open = false;
|
|
1013
|
+
this.text = '';
|
|
1014
|
+
this.bytes = 0;
|
|
1015
|
+
this.body = 0;
|
|
1016
|
+
this.bodyBytes = 0;
|
|
1017
|
+
this.firstLine = 0;
|
|
1018
|
+
this.prefixLines = 0;
|
|
1019
|
+
this.recordAt = 0;
|
|
1020
|
+
this.recordRow = 0;
|
|
1021
|
+
this.lastEnd = 0;
|
|
1022
|
+
this.guarded = new guard_1.Guarded(sink, options.limits, options.abort, options.metrics);
|
|
1023
|
+
this.abort = options.abort;
|
|
1024
|
+
this.maxRecordBytes = options.limits.max_record_bytes;
|
|
1025
|
+
this.chunkBytes = options.chunkBytes;
|
|
1026
|
+
this.wantHeader = header;
|
|
1027
|
+
this.parser = (0, csv_1.make)({ ...grammar, header });
|
|
1028
|
+
this.recordEmpty = !!this.parser.options.plugin?.csv?.record?.empty;
|
|
1029
|
+
const abort = options.abort;
|
|
1030
|
+
(0, rule_events_1.prepare)(this.parser, () => !abort.isAborted());
|
|
1031
|
+
const lexis = Lexis.csv(this.parser);
|
|
1032
|
+
this.scanner = new Scanner(lexis);
|
|
1033
|
+
const splitter = new PieceSplitter(lexis.line, lexis.rows, lexis.single, lexis.fixed);
|
|
1034
|
+
splitter.budget = () => this.maxRecordBytes - (this.open ? this.bytes - this.recordAt : 0);
|
|
1035
|
+
splitter.over = (row) => {
|
|
1036
|
+
const first = (this.open && this.recordRow) || row;
|
|
1037
|
+
return error_1.Fail.limit('max_record_bytes', this.maxRecordBytes, `the record starting at line ${first} is longer than ${this.maxRecordBytes} bytes`).at(first, 1);
|
|
1038
|
+
};
|
|
1039
|
+
this.splitter = splitter;
|
|
1040
|
+
}
|
|
1041
|
+
begin() {
|
|
1042
|
+
return this.guarded.event(event_1.Ev.arrayStart);
|
|
1043
|
+
}
|
|
1044
|
+
// Opens a chunk: after the first, the header goes in front of it.
|
|
1045
|
+
openChunk() {
|
|
1046
|
+
this.open = true;
|
|
1047
|
+
this.text = this.started ? (this.header ?? '') : '';
|
|
1048
|
+
this.bytes = this.started ? this.headerBytes : 0;
|
|
1049
|
+
this.started = true;
|
|
1050
|
+
this.prefixLines = advance(this.scanner.lexis.rows, 0, 0, this.text)[0];
|
|
1051
|
+
this.body = this.text.length;
|
|
1052
|
+
this.bodyBytes = this.bytes;
|
|
1053
|
+
this.firstLine = 0;
|
|
1054
|
+
this.recordAt = this.bytes;
|
|
1055
|
+
this.recordRow = 0;
|
|
1056
|
+
this.lastEnd = 0;
|
|
1057
|
+
}
|
|
1058
|
+
piece(row, text, bytes) {
|
|
1059
|
+
if (!this.open)
|
|
1060
|
+
this.openChunk();
|
|
1061
|
+
if (0 === this.firstLine)
|
|
1062
|
+
this.firstLine = row;
|
|
1063
|
+
if (0 === this.recordRow)
|
|
1064
|
+
this.recordRow = row;
|
|
1065
|
+
const at = this.text.length;
|
|
1066
|
+
const atBytes = this.bytes;
|
|
1067
|
+
this.text += text;
|
|
1068
|
+
this.bytes += bytes;
|
|
1069
|
+
const seeking = this.wantHeader && null === this.header;
|
|
1070
|
+
const found = { from: -1, to: -1 };
|
|
1071
|
+
const ended = this.scanner.piece(text, (end, content) => {
|
|
1072
|
+
if (seeking && found.to < 0 && (content || this.recordEmpty)) {
|
|
1073
|
+
found.from = this.lastEnd;
|
|
1074
|
+
found.to = at + end;
|
|
1075
|
+
}
|
|
1076
|
+
this.lastEnd = at + end;
|
|
1077
|
+
});
|
|
1078
|
+
if (0 <= found.to) {
|
|
1079
|
+
const { from, to } = found;
|
|
1080
|
+
this.header = this.text.slice(from, to);
|
|
1081
|
+
this.headerBytes = utf8Bytes(this.header);
|
|
1082
|
+
this.body = to;
|
|
1083
|
+
this.bodyBytes = atBytes + utf8Bytes(text.slice(0, to - at));
|
|
1084
|
+
}
|
|
1085
|
+
// A chunk may close where a piece ends a record, once the header is
|
|
1086
|
+
// behind it.
|
|
1087
|
+
if (ended && !(this.wantHeader && null === this.header)) {
|
|
1088
|
+
this.recordAt = this.bytes;
|
|
1089
|
+
this.recordRow = 0;
|
|
1090
|
+
if (this.bytes > this.bodyBytes && this.bytes >= this.chunkBytes)
|
|
1091
|
+
return this.flush();
|
|
1092
|
+
}
|
|
1093
|
+
return 'continue';
|
|
1094
|
+
}
|
|
1095
|
+
// Parse the chunk and walk its records.
|
|
1096
|
+
flush() {
|
|
1097
|
+
if (!this.open)
|
|
1098
|
+
return 'continue';
|
|
1099
|
+
const text = this.text;
|
|
1100
|
+
const firstLine = this.firstLine;
|
|
1101
|
+
const prefixLines = this.prefixLines;
|
|
1102
|
+
this.open = false;
|
|
1103
|
+
this.text = '';
|
|
1104
|
+
let value;
|
|
1105
|
+
try {
|
|
1106
|
+
value = this.parser.parse(text);
|
|
1107
|
+
}
|
|
1108
|
+
catch (err) {
|
|
1109
|
+
const pos = (0, error_1.enginePosition)(err);
|
|
1110
|
+
const rowInChunk = Math.max(pos?.row ?? 0, 1);
|
|
1111
|
+
// An error in the prepended header itself is the file's first line.
|
|
1112
|
+
const line = rowInChunk > prefixLines ? firstLine + rowInChunk - prefixLines - 1 : 1;
|
|
1113
|
+
throw lineFailure(err, line, firstLine, this.abort);
|
|
1114
|
+
}
|
|
1115
|
+
// The grammar returns an array of records; anything else has none.
|
|
1116
|
+
const records = Array.isArray(value) ? value : [];
|
|
1117
|
+
try {
|
|
1118
|
+
for (const record of records) {
|
|
1119
|
+
if ('stop' === (0, source_1.walkValue)(record, this.guarded))
|
|
1120
|
+
return 'stop';
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
catch (err) {
|
|
1124
|
+
throw atRecord(err, firstLine);
|
|
1125
|
+
}
|
|
1126
|
+
return 'continue';
|
|
1127
|
+
}
|
|
1128
|
+
finish() {
|
|
1129
|
+
if ('stop' === this.flush())
|
|
1130
|
+
return 'stop';
|
|
1131
|
+
if ('stop' === this.guarded.event(event_1.Ev.arrayEnd))
|
|
1132
|
+
return 'stop';
|
|
1133
|
+
return this.guarded.event(event_1.Ev.end);
|
|
1134
|
+
}
|
|
1135
|
+
close() {
|
|
1136
|
+
this.guarded.flush();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
//# sourceMappingURL=lines.js.map
|