@graphty/graph-io 0.3.40 → 0.3.42
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/README.md +2 -0
- package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
- package/dist/chunks/children-CA4coBi0.js.map +1 -0
- package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
- package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
- package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
- package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
- package/dist/chunks/export-rKLeGQMo.js.map +1 -0
- package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
- package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
- package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
- package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
- package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
- package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
- package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
- package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
- package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
- package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
- package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
- package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
- package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
- package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
- package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
- package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
- package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
- package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
- package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
- package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
- package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
- package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
- package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
- package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
- package/dist/chunks/text-DHOJXnED.js.map +1 -0
- package/dist/csv.js +323 -241
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +195 -97
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +170 -149
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +27 -27
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +103 -65
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/children.d.ts.map +1 -1
- package/dist/src/children.js +50 -27
- package/dist/src/children.js.map +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +86 -42
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +358 -184
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/lists.js +30 -15
- package/dist/src/common/lists.js.map +1 -1
- package/dist/src/common/text.d.ts +7 -0
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +15 -7
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +36 -19
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +288 -146
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +129 -80
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +78 -36
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +11 -8
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/dot/tokenizer.d.ts +39 -0
- package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
- package/dist/src/formats/dot/tokenizer.js +112 -66
- package/dist/src/formats/dot/tokenizer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +65 -37
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +138 -50
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +51 -35
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +103 -58
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +17 -8
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +34 -24
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +17 -13
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/tree.js +23 -14
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +22 -10
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/dialect.d.ts.map +1 -1
- package/dist/src/formats/json/dialect.js +54 -20
- package/dist/src/formats/json/dialect.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +38 -21
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +536 -343
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.js +29 -17
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +73 -43
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +37 -16
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +21 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts.map +1 -1
- package/dist/src/formats/obo/exporter.js +18 -7
- package/dist/src/formats/obo/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -73
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +26 -15
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +85 -58
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +7 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +79 -40
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +38 -12
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +38 -18
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/sniff.js +109 -57
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/children.ts +54 -27
- package/src/common/attributes.ts +118 -49
- package/src/common/input.ts +439 -197
- package/src/common/lists.ts +29 -15
- package/src/common/text.ts +24 -15
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +377 -175
- package/src/formats/csv/importer.ts +155 -84
- package/src/formats/csv/records.ts +90 -35
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/dot/tokenizer.ts +118 -68
- package/src/formats/gexf/exporter.ts +67 -36
- package/src/formats/gexf/importer.ts +173 -63
- package/src/formats/gml/exporter.ts +78 -82
- package/src/formats/gml/importer.ts +119 -68
- package/src/formats/gml/syntax.ts +18 -8
- package/src/formats/graphml/exporter.ts +44 -24
- package/src/formats/graphml/importer.ts +18 -11
- package/src/formats/graphml/tree.ts +22 -12
- package/src/formats/graphml/yfiles.ts +23 -10
- package/src/formats/json/dialect.ts +61 -22
- package/src/formats/json/exporter.ts +40 -21
- package/src/formats/json/importer.ts +701 -385
- package/src/formats/json/obographs.ts +35 -15
- package/src/formats/neo4j/exporter.ts +79 -41
- package/src/formats/neo4j/header.ts +50 -16
- package/src/formats/neo4j/importer.ts +21 -10
- package/src/formats/obo/exporter.ts +34 -17
- package/src/formats/pajek/exporter.ts +180 -117
- package/src/formats/pajek/importer.ts +27 -15
- package/src/formats/pajek/syntax.ts +99 -58
- package/src/formats/xgmml/emit.ts +88 -40
- package/src/formats/xgmml/exporter.ts +40 -12
- package/src/formats/xgmml/importer.ts +45 -23
- package/src/sniff.ts +115 -59
- package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
- package/dist/chunks/export-B-aVCwyG.js.map +0 -1
- package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
- package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
- package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
- package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
- package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
- package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
- package/dist/chunks/importer-DcKB97kW.js.map +0 -1
- package/dist/chunks/records-r3SFHz11.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
package/src/common/input.ts
CHANGED
|
@@ -506,46 +506,81 @@ class ByteDecoder {
|
|
|
506
506
|
const bad = this.encoding === "utf-8" ? invalidUtf8(all) : null;
|
|
507
507
|
const at = base + (bad === null ? 0 : bad.index);
|
|
508
508
|
if (this.mayFallBack && this.asciiSoFar) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
return this.binary(this.control);
|
|
513
|
-
}
|
|
514
|
-
if (this.textFollows && bad?.cut === true) {
|
|
515
|
-
return this.report.fail(
|
|
516
|
-
INVALID_UTF8_CODE,
|
|
517
|
-
`invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`,
|
|
518
|
-
undefined,
|
|
519
|
-
{ byteOffset: at },
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
if (!stream && bad?.truncated === true) {
|
|
523
|
-
return this.report.fail(
|
|
524
|
-
INVALID_UTF8_CODE,
|
|
525
|
-
`the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`,
|
|
526
|
-
undefined,
|
|
527
|
-
{ byteOffset: at },
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
if (!startsWithUtf8(all)) {
|
|
531
|
-
const cutNote =
|
|
532
|
-
!stream && bad?.cut === true
|
|
533
|
-
? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated"
|
|
534
|
-
: "";
|
|
535
|
-
this.report.warning(
|
|
536
|
-
"coercion",
|
|
537
|
-
ENCODING_FALLBACK_CODE,
|
|
538
|
-
this.declaredUtf8
|
|
539
|
-
? `the input declares UTF-8 but is not valid UTF-8 (at byte ${at}); everything before was ASCII, so it is read as windows-1252 (pass the encoding option to choose another)`
|
|
540
|
-
: `the input is not valid UTF-8 (at byte ${at}${cutNote}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`,
|
|
541
|
-
);
|
|
542
|
-
this.use("windows-1252", false);
|
|
543
|
-
this.fellBack = true;
|
|
544
|
-
this.mixedPending = true;
|
|
545
|
-
this.reportUtf8After(all, base);
|
|
546
|
-
return (this.decoder as TextDecoder).decode(all, { stream });
|
|
509
|
+
const text = this.fallBack(all, bad, at, base, stream);
|
|
510
|
+
if (text !== null) {
|
|
511
|
+
return text;
|
|
547
512
|
}
|
|
548
513
|
}
|
|
514
|
+
return this.failDecode(bytes, at, base, stream);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Undeclared input that was ASCII so far failed to decode: fail on binary data or a cut
|
|
519
|
+
* sequence, else switch to windows-1252 with a warning unless the bytes start with valid
|
|
520
|
+
* non-ASCII UTF-8.
|
|
521
|
+
* @param all - the held-back bytes followed by the bytes that failed
|
|
522
|
+
* @param bad - the first invalid UTF-8 sequence, or null when the encoding is not UTF-8
|
|
523
|
+
* @param at - the byte offset of the first invalid byte
|
|
524
|
+
* @param base - the byte offset of `all`
|
|
525
|
+
* @param stream - whether more bytes follow
|
|
526
|
+
* @returns the bytes decoded as windows-1252, or null when they do not fall back
|
|
527
|
+
*/
|
|
528
|
+
private fallBack(
|
|
529
|
+
all: Uint8Array,
|
|
530
|
+
bad: Utf8Problem | null,
|
|
531
|
+
at: number,
|
|
532
|
+
base: number,
|
|
533
|
+
stream: boolean,
|
|
534
|
+
): string | null {
|
|
535
|
+
// windows-1252 text holds no control bytes but TAB, LF, FF and CR: one anywhere in
|
|
536
|
+
// undeclared input that is not UTF-8 marks binary data (watch() saw every byte so far)
|
|
537
|
+
if (this.control !== null) {
|
|
538
|
+
return this.binary(this.control);
|
|
539
|
+
}
|
|
540
|
+
if (this.textFollows && bad?.cut === true) {
|
|
541
|
+
return this.report.fail(
|
|
542
|
+
INVALID_UTF8_CODE,
|
|
543
|
+
`invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`,
|
|
544
|
+
undefined,
|
|
545
|
+
{ byteOffset: at },
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (!stream && bad?.truncated === true) {
|
|
549
|
+
return this.report.fail(
|
|
550
|
+
INVALID_UTF8_CODE,
|
|
551
|
+
`the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`,
|
|
552
|
+
undefined,
|
|
553
|
+
{ byteOffset: at },
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
if (startsWithUtf8(all)) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const cutNote =
|
|
560
|
+
!stream && bad?.cut === true ? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated" : "";
|
|
561
|
+
this.report.warning(
|
|
562
|
+
"coercion",
|
|
563
|
+
ENCODING_FALLBACK_CODE,
|
|
564
|
+
this.declaredUtf8
|
|
565
|
+
? `the input declares UTF-8 but is not valid UTF-8 (at byte ${at}); everything before was ASCII, so it is read as windows-1252 (pass the encoding option to choose another)`
|
|
566
|
+
: `the input is not valid UTF-8 (at byte ${at}${cutNote}) and declares no encoding; read as windows-1252 (pass the encoding option to choose another)`,
|
|
567
|
+
);
|
|
568
|
+
this.use("windows-1252", false);
|
|
569
|
+
this.fellBack = true;
|
|
570
|
+
this.mixedPending = true;
|
|
571
|
+
this.reportUtf8After(all, base);
|
|
572
|
+
return (this.decoder as TextDecoder).decode(all, { stream });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Fail the import on bytes that are not valid in the encoding.
|
|
577
|
+
* @param bytes - the bytes that failed
|
|
578
|
+
* @param at - the byte offset of the first invalid UTF-8 byte
|
|
579
|
+
* @param base - the byte offset of the held-back bytes and these
|
|
580
|
+
* @param stream - whether more bytes follow
|
|
581
|
+
* @returns never: it throws
|
|
582
|
+
*/
|
|
583
|
+
private failDecode(bytes: Uint8Array, at: number, base: number, stream: boolean): never {
|
|
549
584
|
if (this.encoding === "utf-8") {
|
|
550
585
|
const after = this.mayFallBack ? " after valid non-ASCII UTF-8 text; pass the encoding option" : "";
|
|
551
586
|
return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}${after}`, undefined, {
|
|
@@ -1232,130 +1267,244 @@ export async function* textChunks(
|
|
|
1232
1267
|
): AsyncGenerator<string, void, undefined> {
|
|
1233
1268
|
const input = normalizeInput(rawInput);
|
|
1234
1269
|
const signal = options.signal ?? null;
|
|
1235
|
-
const onProgress = options.onProgress ?? null;
|
|
1236
1270
|
const filter = new TextFilter(report, options);
|
|
1237
1271
|
throwIfAborted(signal);
|
|
1238
1272
|
if (typeof input === "string") {
|
|
1239
1273
|
reportTextEncoding(report, options.encoding);
|
|
1240
|
-
|
|
1241
|
-
let done = 0;
|
|
1242
|
-
let start = 0;
|
|
1243
|
-
while (start < input.length) {
|
|
1244
|
-
throwIfAborted(signal);
|
|
1245
|
-
let end = Math.min(start + DECODE_SLICE, input.length);
|
|
1246
|
-
const highSurrogate = (input.charCodeAt(end - 1) & 0xfc00) === 0xd800; // NOSONAR(S7758): reads UTF-16 code units on purpose
|
|
1247
|
-
if (end < input.length && highSurrogate) {
|
|
1248
|
-
end++; // never split a surrogate pair
|
|
1249
|
-
}
|
|
1250
|
-
const piece = start === 0 && end === input.length ? input : input.slice(start, end);
|
|
1251
|
-
start = end;
|
|
1252
|
-
const text = filter.push(piece);
|
|
1253
|
-
if (text.length > 0) {
|
|
1254
|
-
yield text;
|
|
1255
|
-
}
|
|
1256
|
-
if (onProgress !== null) {
|
|
1257
|
-
done += utf8Length(piece);
|
|
1258
|
-
onProgress(done, total);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
|
-
if (input.length === 0) {
|
|
1262
|
-
onProgress?.(0, 0);
|
|
1263
|
-
}
|
|
1264
|
-
filter.end();
|
|
1274
|
+
yield* stringTextChunks(input, filter, options);
|
|
1265
1275
|
return;
|
|
1266
1276
|
}
|
|
1267
1277
|
const decoder = new ByteDecoder(report, options);
|
|
1268
|
-
const headBytes = Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
|
|
1269
1278
|
if (input instanceof Uint8Array) {
|
|
1270
|
-
|
|
1271
|
-
decoder.start(input.subarray(0, headBytes));
|
|
1272
|
-
for (let offset = 0; offset < length; offset += DECODE_SLICE) {
|
|
1273
|
-
throwIfAborted(signal);
|
|
1274
|
-
if (input.byteLength !== length) {
|
|
1275
|
-
throw new GraphFormatError(
|
|
1276
|
-
"E_UNSUPPORTED",
|
|
1277
|
-
"the input's buffer was detached (transferred) or resized during the import",
|
|
1278
|
-
{ reason: "detached" },
|
|
1279
|
-
);
|
|
1280
|
-
}
|
|
1281
|
-
const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
|
|
1282
|
-
const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
|
|
1283
|
-
if (text.length > 0) {
|
|
1284
|
-
yield text;
|
|
1285
|
-
}
|
|
1286
|
-
onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
|
|
1287
|
-
}
|
|
1288
|
-
if (length === 0) {
|
|
1289
|
-
onProgress?.(0, 0);
|
|
1290
|
-
}
|
|
1291
|
-
filter.end();
|
|
1279
|
+
yield* byteTextChunks(input, filter, decoder, options);
|
|
1292
1280
|
return;
|
|
1293
1281
|
}
|
|
1294
|
-
// the
|
|
1295
|
-
//
|
|
1296
|
-
|
|
1297
|
-
|
|
1282
|
+
// the stream's loop stays here rather than in a delegated generator: a yield* layer costs
|
|
1283
|
+
// time on every chunk, and a stream may arrive in many small ones
|
|
1284
|
+
const stream = new StreamText(filter, decoder, options);
|
|
1285
|
+
for await (const chunk of isReadableStream(input) ? streamChunks(input, signal) : input) {
|
|
1286
|
+
throwIfAborted(signal);
|
|
1287
|
+
const text = stream.push(chunk);
|
|
1288
|
+
if (text.length > 0) {
|
|
1289
|
+
yield text;
|
|
1290
|
+
}
|
|
1291
|
+
options.onProgress?.(stream.done);
|
|
1292
|
+
}
|
|
1293
|
+
const tail = stream.end();
|
|
1294
|
+
if (tail.length > 0) {
|
|
1295
|
+
yield tail;
|
|
1296
|
+
}
|
|
1297
|
+
filter.end();
|
|
1298
|
+
options.onProgress?.(stream.done, stream.done);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* The end of the slice of a string input that starts at start: DECODE_SLICE code units, one more
|
|
1303
|
+
* when the slice would end between the two halves of a surrogate pair.
|
|
1304
|
+
* @param input - the string
|
|
1305
|
+
* @param start - where the slice starts
|
|
1306
|
+
* @returns where it ends
|
|
1307
|
+
*/
|
|
1308
|
+
function stringSliceEnd(input: string, start: number): number {
|
|
1309
|
+
const end = Math.min(start + DECODE_SLICE, input.length);
|
|
1310
|
+
const highSurrogate = (input.charCodeAt(end - 1) & 0xfc00) === 0xd800; // NOSONAR(S7758): reads UTF-16 code units on purpose
|
|
1311
|
+
return end < input.length && highSurrogate ? end + 1 : end;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
/**
|
|
1315
|
+
* textChunks() of a string input: slices of it, through the text filter.
|
|
1316
|
+
* @param input - the string
|
|
1317
|
+
* @param filter - the text checks
|
|
1318
|
+
* @param options - cancellation and progress
|
|
1319
|
+
* @yields the filtered slices
|
|
1320
|
+
* @returns nothing
|
|
1321
|
+
*/
|
|
1322
|
+
function* stringTextChunks(
|
|
1323
|
+
input: string,
|
|
1324
|
+
filter: TextFilter,
|
|
1325
|
+
options: ReadOptions,
|
|
1326
|
+
): Generator<string, void, undefined> {
|
|
1327
|
+
const signal = options.signal ?? null;
|
|
1328
|
+
const onProgress = options.onProgress ?? null;
|
|
1329
|
+
const total = onProgress === null ? 0 : utf8Length(input);
|
|
1298
1330
|
let done = 0;
|
|
1299
|
-
let
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1331
|
+
let start = 0;
|
|
1332
|
+
while (start < input.length) {
|
|
1333
|
+
throwIfAborted(signal);
|
|
1334
|
+
const end = stringSliceEnd(input, start);
|
|
1335
|
+
const piece = start === 0 && end === input.length ? input : input.slice(start, end);
|
|
1336
|
+
start = end;
|
|
1337
|
+
const text = filter.push(piece);
|
|
1338
|
+
if (text.length > 0) {
|
|
1339
|
+
yield text;
|
|
1340
|
+
}
|
|
1341
|
+
if (onProgress !== null) {
|
|
1342
|
+
done += utf8Length(piece);
|
|
1343
|
+
onProgress(done, total);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
if (input.length === 0) {
|
|
1347
|
+
onProgress?.(0, 0);
|
|
1348
|
+
}
|
|
1349
|
+
filter.end();
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
/**
|
|
1353
|
+
* textChunks() of a Uint8Array input: slices of it, decoded, through the text filter.
|
|
1354
|
+
* @param input - the bytes
|
|
1355
|
+
* @param filter - the text checks
|
|
1356
|
+
* @param decoder - the byte decoder
|
|
1357
|
+
* @param options - cancellation, progress and the declaration size
|
|
1358
|
+
* @yields the decoded, filtered slices
|
|
1359
|
+
* @returns nothing
|
|
1360
|
+
*/
|
|
1361
|
+
function* byteTextChunks(
|
|
1362
|
+
input: Uint8Array,
|
|
1363
|
+
filter: TextFilter,
|
|
1364
|
+
decoder: ByteDecoder,
|
|
1365
|
+
options: ReadOptions,
|
|
1366
|
+
): Generator<string, void, undefined> {
|
|
1367
|
+
const signal = options.signal ?? null;
|
|
1368
|
+
const length = input.byteLength;
|
|
1369
|
+
decoder.start(input.subarray(0, headByteCount(options)));
|
|
1370
|
+
for (let offset = 0; offset < length; offset += DECODE_SLICE) {
|
|
1310
1371
|
throwIfAborted(signal);
|
|
1311
|
-
|
|
1312
|
-
|
|
1372
|
+
if (input.byteLength !== length) {
|
|
1373
|
+
throw new GraphFormatError(
|
|
1374
|
+
"E_UNSUPPORTED",
|
|
1375
|
+
"the input's buffer was detached (transferred) or resized during the import",
|
|
1376
|
+
{ reason: "detached" },
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
|
|
1380
|
+
const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
|
|
1381
|
+
if (text.length > 0) {
|
|
1382
|
+
yield text;
|
|
1383
|
+
}
|
|
1384
|
+
options.onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
|
|
1385
|
+
}
|
|
1386
|
+
if (length === 0) {
|
|
1387
|
+
options.onProgress?.(0, 0);
|
|
1388
|
+
}
|
|
1389
|
+
filter.end();
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/**
|
|
1393
|
+
* How many leading bytes the encoding is chosen from: the format's declaration size, at least a BOM.
|
|
1394
|
+
* @param options - the declaration size
|
|
1395
|
+
* @returns the byte count
|
|
1396
|
+
*/
|
|
1397
|
+
function headByteCount(options: ReadOptions): number {
|
|
1398
|
+
return Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* The chunks of a stream input (a ReadableStream or an async iterable of strings and bytes) turned
|
|
1403
|
+
* into filtered text. The first bytes are held back until the BOM check (and the declaration
|
|
1404
|
+
* check, when the format has one) can see enough of them, or the input ended, or text arrived, so
|
|
1405
|
+
* a BOM or a declaration split across small chunks is still seen.
|
|
1406
|
+
*/
|
|
1407
|
+
class StreamText {
|
|
1408
|
+
/** Bytes read so far (text counted as UTF-8 when progress is reported), for progress. */
|
|
1409
|
+
done = 0;
|
|
1410
|
+
|
|
1411
|
+
private head: Uint8Array[] = [];
|
|
1412
|
+
|
|
1413
|
+
private headLength = 0;
|
|
1414
|
+
|
|
1415
|
+
private readonly headBytes: number;
|
|
1416
|
+
|
|
1417
|
+
/** How many bytes to hold back before choosing the encoding. */
|
|
1418
|
+
private readonly needed: number;
|
|
1419
|
+
|
|
1420
|
+
/** Whether progress is reported, so text chunks are measured. */
|
|
1421
|
+
private readonly counting: boolean;
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* Create the reader of one stream input.
|
|
1425
|
+
* @param filter - the text checks
|
|
1426
|
+
* @param decoder - the byte decoder
|
|
1427
|
+
* @param options - progress, the declaration reader and its size
|
|
1428
|
+
*/
|
|
1429
|
+
constructor(
|
|
1430
|
+
private readonly filter: TextFilter,
|
|
1431
|
+
private readonly decoder: ByteDecoder,
|
|
1432
|
+
options: ReadOptions,
|
|
1433
|
+
) {
|
|
1434
|
+
this.headBytes = headByteCount(options);
|
|
1435
|
+
this.needed = options.declaredEncoding === undefined ? BOM_BYTES : this.headBytes;
|
|
1436
|
+
this.counting = (options.onProgress ?? null) !== null;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Read the next chunk.
|
|
1441
|
+
* @param chunk - a string or a Uint8Array (anything else is E_UNSUPPORTED)
|
|
1442
|
+
* @returns its filtered text; empty while bytes are held back
|
|
1443
|
+
*/
|
|
1444
|
+
push(chunk: unknown): string {
|
|
1313
1445
|
if (typeof chunk === "string") {
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
pending = decoder.flushBeforeText(() => flushHead(false));
|
|
1320
|
-
} else if (decoder.started) {
|
|
1321
|
-
pending = decoder.flushBeforeText(() => decoder.decode(new Uint8Array(0), false));
|
|
1322
|
-
} else {
|
|
1323
|
-
decoder.start(new Uint8Array(0));
|
|
1324
|
-
}
|
|
1325
|
-
text = filter.push(pending + chunk);
|
|
1326
|
-
done += onProgress === null ? 0 : utf8Length(chunk);
|
|
1327
|
-
} else if (bytes !== null) {
|
|
1328
|
-
done += bytes.byteLength;
|
|
1329
|
-
if (decoder.started) {
|
|
1330
|
-
text = filter.push(decoder.decode(bytes, true));
|
|
1331
|
-
} else {
|
|
1332
|
-
head.push(bytes);
|
|
1333
|
-
headLength += bytes.byteLength;
|
|
1334
|
-
text = headLength >= needed ? filter.push(flushHead(true)) : "";
|
|
1335
|
-
}
|
|
1336
|
-
} else {
|
|
1446
|
+
this.done += this.counting ? utf8Length(chunk) : 0;
|
|
1447
|
+
return this.filter.push(this.beforeText() + chunk);
|
|
1448
|
+
}
|
|
1449
|
+
const bytes = asBytes(chunk);
|
|
1450
|
+
if (bytes === null) {
|
|
1337
1451
|
throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
|
|
1338
1452
|
reason: "chunk type",
|
|
1339
1453
|
found: typeof chunk,
|
|
1340
1454
|
});
|
|
1341
1455
|
}
|
|
1342
|
-
|
|
1343
|
-
|
|
1456
|
+
this.done += bytes.byteLength;
|
|
1457
|
+
if (this.decoder.started) {
|
|
1458
|
+
return this.filter.push(this.decoder.decode(bytes, true));
|
|
1459
|
+
}
|
|
1460
|
+
this.head.push(bytes);
|
|
1461
|
+
this.headLength += bytes.byteLength;
|
|
1462
|
+
return this.headLength >= this.needed ? this.filter.push(this.flushHead(true)) : "";
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/**
|
|
1466
|
+
* The input ended: the filtered text of whatever is still held or pending.
|
|
1467
|
+
* @returns the text
|
|
1468
|
+
*/
|
|
1469
|
+
end(): string {
|
|
1470
|
+
let tail = "";
|
|
1471
|
+
if (this.headLength > 0) {
|
|
1472
|
+
tail = this.flushHead(false);
|
|
1473
|
+
} else if (this.decoder.started) {
|
|
1474
|
+
tail = this.decoder.decode(new Uint8Array(0), false);
|
|
1344
1475
|
}
|
|
1345
|
-
|
|
1476
|
+
return this.filter.push(tail);
|
|
1346
1477
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1478
|
+
|
|
1479
|
+
/**
|
|
1480
|
+
* Start the decoder on the held bytes and decode them.
|
|
1481
|
+
* @param stream - whether more bytes follow
|
|
1482
|
+
* @returns their text
|
|
1483
|
+
*/
|
|
1484
|
+
private flushHead(stream: boolean): string {
|
|
1485
|
+
const bytes = concatBytes(this.head);
|
|
1486
|
+
this.head = [];
|
|
1487
|
+
this.headLength = 0;
|
|
1488
|
+
this.decoder.start(bytes.subarray(0, this.headBytes));
|
|
1489
|
+
return this.decoder.decode(bytes, stream);
|
|
1352
1490
|
}
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* A text chunk follows: finish any byte sequence still pending in the decoder before switching
|
|
1494
|
+
* to text. The BOM and declaration checks apply to the start of the input only, so bytes after
|
|
1495
|
+
* text are never taken for a BOM.
|
|
1496
|
+
* @returns the text of the pending bytes
|
|
1497
|
+
*/
|
|
1498
|
+
private beforeText(): string {
|
|
1499
|
+
if (this.headLength > 0) {
|
|
1500
|
+
return this.decoder.flushBeforeText(() => this.flushHead(false));
|
|
1501
|
+
}
|
|
1502
|
+
if (this.decoder.started) {
|
|
1503
|
+
return this.decoder.flushBeforeText(() => this.decoder.decode(new Uint8Array(0), false));
|
|
1504
|
+
}
|
|
1505
|
+
this.decoder.start(new Uint8Array(0));
|
|
1506
|
+
return "";
|
|
1356
1507
|
}
|
|
1357
|
-
filter.end();
|
|
1358
|
-
onProgress?.(done, done);
|
|
1359
1508
|
}
|
|
1360
1509
|
|
|
1361
1510
|
/**
|
|
@@ -1548,81 +1697,174 @@ export class LineReader implements AsyncIterable<string> {
|
|
|
1548
1697
|
* @returns nothing
|
|
1549
1698
|
*/
|
|
1550
1699
|
async *[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined> {
|
|
1551
|
-
// The pieces of the line in progress
|
|
1552
|
-
//
|
|
1553
|
-
|
|
1554
|
-
const pending: string[] = [];
|
|
1555
|
-
let pendingLength = 0;
|
|
1700
|
+
// The pieces of the line in progress, joined once when the line ends, so a line spanning
|
|
1701
|
+
// many chunks costs its length, not its length times the chunk count.
|
|
1702
|
+
const pending = new PendingLine((line) => tooLarge(this.report, this.lineTooLong(), line));
|
|
1556
1703
|
let trailingCr = false;
|
|
1557
1704
|
for await (const chunk of textChunks(this.input, this.report, this.options)) {
|
|
1558
1705
|
let start = 0;
|
|
1559
|
-
const end = chunk.length;
|
|
1560
1706
|
if (trailingCr) {
|
|
1561
1707
|
// the previous chunk ended with `\r`: that ended a line, and a leading `\n` here is
|
|
1562
1708
|
// the second half of the same terminator
|
|
1563
1709
|
trailingCr = false;
|
|
1564
1710
|
this.lineNumber++;
|
|
1565
|
-
|
|
1566
|
-
pending.length = 0;
|
|
1567
|
-
pendingLength = 0;
|
|
1568
|
-
yield joined.slice(0, -1);
|
|
1711
|
+
yield pending.take().slice(0, -1);
|
|
1569
1712
|
if (chunk.charCodeAt(0) === 10) {
|
|
1570
1713
|
start = 1;
|
|
1571
1714
|
}
|
|
1572
1715
|
}
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
let nl = chunk.indexOf("\n", start);
|
|
1576
|
-
let cr = chunk.indexOf("\r", start);
|
|
1577
|
-
while (nl >= 0 || cr >= 0) {
|
|
1578
|
-
let cut: number;
|
|
1579
|
-
let next: number;
|
|
1580
|
-
if (cr >= 0 && (nl < 0 || cr < nl)) {
|
|
1581
|
-
if (cr === end - 1) {
|
|
1582
|
-
// a trailing \r may be the first half of \r\n split across chunks
|
|
1583
|
-
break;
|
|
1584
|
-
}
|
|
1585
|
-
cut = cr;
|
|
1586
|
-
next = chunk.charCodeAt(cr + 1) === 10 ? cr + 2 : cr + 1;
|
|
1587
|
-
} else {
|
|
1588
|
-
cut = nl;
|
|
1589
|
-
next = nl + 1;
|
|
1590
|
-
}
|
|
1716
|
+
const breaks = new LineBreaks(chunk, start);
|
|
1717
|
+
while (breaks.advance()) {
|
|
1591
1718
|
this.lineNumber++;
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
yield piece;
|
|
1595
|
-
} else {
|
|
1596
|
-
if (pendingLength + piece.length > MAX_TEXT_LENGTH) {
|
|
1597
|
-
tooLarge(this.report, this.lineTooLong(), this.lineNumber);
|
|
1598
|
-
}
|
|
1599
|
-
pending.push(piece);
|
|
1600
|
-
const joined = pending.join("");
|
|
1601
|
-
pending.length = 0;
|
|
1602
|
-
pendingLength = 0;
|
|
1603
|
-
yield joined;
|
|
1604
|
-
}
|
|
1605
|
-
start = next;
|
|
1606
|
-
if (nl >= 0 && nl < next) {
|
|
1607
|
-
nl = chunk.indexOf("\n", next);
|
|
1608
|
-
}
|
|
1609
|
-
if (cr >= 0 && cr < next) {
|
|
1610
|
-
cr = chunk.indexOf("\r", next);
|
|
1611
|
-
}
|
|
1719
|
+
yield pending.finish(chunk.slice(start, breaks.cut), this.lineNumber);
|
|
1720
|
+
start = breaks.next;
|
|
1612
1721
|
}
|
|
1613
|
-
if (start <
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
tooLarge(this.report, this.lineTooLong(), this.lineNumber + 1);
|
|
1617
|
-
}
|
|
1618
|
-
pending.push(start === 0 ? chunk : chunk.slice(start));
|
|
1619
|
-
trailingCr = chunk.charCodeAt(end - 1) === 13;
|
|
1722
|
+
if (start < chunk.length) {
|
|
1723
|
+
pending.appendRest(chunk, start, this.lineNumber + 1);
|
|
1724
|
+
trailingCr = chunk.codePointAt(chunk.length - 1) === 13;
|
|
1620
1725
|
}
|
|
1621
1726
|
}
|
|
1622
|
-
if (pending.
|
|
1727
|
+
if (!pending.empty) {
|
|
1623
1728
|
this.lineNumber++;
|
|
1624
|
-
const joined = pending.
|
|
1729
|
+
const joined = pending.take();
|
|
1625
1730
|
yield joined.endsWith("\r") ? joined.slice(0, -1) : joined;
|
|
1626
1731
|
}
|
|
1627
1732
|
}
|
|
1628
1733
|
}
|
|
1734
|
+
|
|
1735
|
+
/**
|
|
1736
|
+
* The pieces of a line that spans chunks. None of them holds a line break, except that the last
|
|
1737
|
+
* may end with a `\r` whose meaning the next chunk decides.
|
|
1738
|
+
*/
|
|
1739
|
+
class PendingLine {
|
|
1740
|
+
private readonly pieces: string[] = [];
|
|
1741
|
+
|
|
1742
|
+
private length = 0;
|
|
1743
|
+
|
|
1744
|
+
private readonly tooLong: (line: number) => never;
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Start an empty line.
|
|
1748
|
+
* @param tooLong - fails the import when the line grows longer than one string can hold
|
|
1749
|
+
*/
|
|
1750
|
+
constructor(tooLong: (line: number) => never) {
|
|
1751
|
+
this.tooLong = tooLong;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Whether no piece is held.
|
|
1756
|
+
* @returns true when the line in progress is empty
|
|
1757
|
+
*/
|
|
1758
|
+
get empty(): boolean {
|
|
1759
|
+
return this.pieces.length === 0;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Hold the rest of a chunk from `start`: the line goes on in the next chunk.
|
|
1764
|
+
* @param chunk - the chunk
|
|
1765
|
+
* @param start - where the unfinished line starts in it
|
|
1766
|
+
* @param line - the 1-based number of the line, for the too-long error
|
|
1767
|
+
*/
|
|
1768
|
+
appendRest(chunk: string, start: number, line: number): void {
|
|
1769
|
+
this.append(start === 0 ? chunk : chunk.slice(start), line);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* End the line with its last piece.
|
|
1774
|
+
* @param piece - the text from the start of the chunk (or the last break) to the line break
|
|
1775
|
+
* @param line - the 1-based number of the line, for the too-long error
|
|
1776
|
+
* @returns the whole line
|
|
1777
|
+
*/
|
|
1778
|
+
finish(piece: string, line: number): string {
|
|
1779
|
+
if (this.pieces.length === 0) {
|
|
1780
|
+
return piece;
|
|
1781
|
+
}
|
|
1782
|
+
this.append(piece, line);
|
|
1783
|
+
return this.take();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Join the held pieces and start a new line.
|
|
1788
|
+
* @returns the joined text
|
|
1789
|
+
*/
|
|
1790
|
+
take(): string {
|
|
1791
|
+
const joined = this.pieces.join("");
|
|
1792
|
+
this.pieces.length = 0;
|
|
1793
|
+
this.length = 0;
|
|
1794
|
+
return joined;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/**
|
|
1798
|
+
* Hold one more piece.
|
|
1799
|
+
* @param piece - the piece
|
|
1800
|
+
* @param line - the 1-based number of the line, for the too-long error
|
|
1801
|
+
*/
|
|
1802
|
+
private append(piece: string, line: number): void {
|
|
1803
|
+
this.length += piece.length;
|
|
1804
|
+
if (this.length > MAX_TEXT_LENGTH) {
|
|
1805
|
+
this.tooLong(line);
|
|
1806
|
+
}
|
|
1807
|
+
this.pieces.push(piece);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/**
|
|
1812
|
+
* The line breaks of one chunk, in order: `\n`, `\r\n` and a lone `\r`. Each of `\n` and `\r` is
|
|
1813
|
+
* searched for once per chunk and again only after it was consumed, so a chunk is scanned once
|
|
1814
|
+
* whatever its line count.
|
|
1815
|
+
*/
|
|
1816
|
+
class LineBreaks {
|
|
1817
|
+
/** Where the line just found ends (its terminator starts there). */
|
|
1818
|
+
cut = 0;
|
|
1819
|
+
|
|
1820
|
+
/** Where the next line starts. */
|
|
1821
|
+
next: number;
|
|
1822
|
+
|
|
1823
|
+
private readonly chunk: string;
|
|
1824
|
+
|
|
1825
|
+
private nl: number;
|
|
1826
|
+
|
|
1827
|
+
private cr: number;
|
|
1828
|
+
|
|
1829
|
+
/**
|
|
1830
|
+
* Scan a chunk from a position.
|
|
1831
|
+
* @param chunk - the chunk
|
|
1832
|
+
* @param start - where scanning starts
|
|
1833
|
+
*/
|
|
1834
|
+
constructor(chunk: string, start: number) {
|
|
1835
|
+
this.chunk = chunk;
|
|
1836
|
+
this.next = start;
|
|
1837
|
+
this.nl = chunk.indexOf("\n", start);
|
|
1838
|
+
this.cr = chunk.indexOf("\r", start);
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* Find the next line break.
|
|
1843
|
+
* @returns false when none is left, or the only one left is a trailing `\r` (it may be the first
|
|
1844
|
+
* half of a `\r\n` split across chunks)
|
|
1845
|
+
*/
|
|
1846
|
+
advance(): boolean {
|
|
1847
|
+
const { chunk, next } = this;
|
|
1848
|
+
if (this.nl >= 0 && this.nl < next) {
|
|
1849
|
+
this.nl = chunk.indexOf("\n", next);
|
|
1850
|
+
}
|
|
1851
|
+
if (this.cr >= 0 && this.cr < next) {
|
|
1852
|
+
this.cr = chunk.indexOf("\r", next);
|
|
1853
|
+
}
|
|
1854
|
+
const { nl, cr } = this;
|
|
1855
|
+
if (cr >= 0 && (nl < 0 || cr < nl)) {
|
|
1856
|
+
if (cr === chunk.length - 1) {
|
|
1857
|
+
return false;
|
|
1858
|
+
}
|
|
1859
|
+
this.cut = cr;
|
|
1860
|
+
this.next = chunk.codePointAt(cr + 1) === 10 ? cr + 2 : cr + 1;
|
|
1861
|
+
return true;
|
|
1862
|
+
}
|
|
1863
|
+
if (nl < 0) {
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
this.cut = nl;
|
|
1867
|
+
this.next = nl + 1;
|
|
1868
|
+
return true;
|
|
1869
|
+
}
|
|
1870
|
+
}
|