@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.
Files changed (210) hide show
  1. package/README.md +2 -0
  2. package/dist/chunks/{children-CL3Cy0ez.js → children-CA4coBi0.js} +36 -27
  3. package/dist/chunks/children-CA4coBi0.js.map +1 -0
  4. package/dist/chunks/{choose-CS3fvGT1.js → choose-DvpbdRuz.js} +2 -2
  5. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-DvpbdRuz.js.map} +1 -1
  6. package/dist/chunks/{constants-DqKZT6cH.js → constants-fT3E8JYY.js} +2 -2
  7. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-fT3E8JYY.js.map} +1 -1
  8. package/dist/chunks/{escape-BJRLd3vJ.js → escape-B62ZGM_a.js} +2 -2
  9. package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-B62ZGM_a.js.map} +1 -1
  10. package/dist/chunks/{export-B-aVCwyG.js → export-rKLeGQMo.js} +404 -243
  11. package/dist/chunks/export-rKLeGQMo.js.map +1 -0
  12. package/dist/chunks/{exporter-6ocL_9x_.js → exporter-8hLNemFk.js} +4 -4
  13. package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-8hLNemFk.js.map} +1 -1
  14. package/dist/chunks/{exporter-BVXnV9-9.js → exporter-BWwEuOPT.js} +29 -17
  15. package/dist/chunks/exporter-BWwEuOPT.js.map +1 -0
  16. package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-CpRyHZ3H.js} +574 -475
  17. package/dist/chunks/exporter-CpRyHZ3H.js.map +1 -0
  18. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-yLxKBvLE.js} +23 -20
  19. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-yLxKBvLE.js.map} +1 -1
  20. package/dist/chunks/{importer-DcKB97kW.js → importer-BbNb7eNg.js} +111 -71
  21. package/dist/chunks/importer-BbNb7eNg.js.map +1 -0
  22. package/dist/chunks/{importer-D_srHyj6.js → importer-C98dpSH4.js} +5 -5
  23. package/dist/chunks/{importer-D_srHyj6.js.map → importer-C98dpSH4.js.map} +1 -1
  24. package/dist/chunks/{importer-BJlGAOht.js → importer-D794XUTe.js} +3 -3
  25. package/dist/chunks/{importer-BJlGAOht.js.map → importer-D794XUTe.js.map} +1 -1
  26. package/dist/chunks/{importer-AjwOJrn2.js → importer-Dz7tI8Sa.js} +202 -181
  27. package/dist/chunks/importer-Dz7tI8Sa.js.map +1 -0
  28. package/dist/chunks/{importer-DEp_fYjf.js → importer-Ht7LNg8L.js} +92 -64
  29. package/dist/chunks/importer-Ht7LNg8L.js.map +1 -0
  30. package/dist/chunks/{importer-CPqnIY8m.js → importer-WrGRZb0N.js} +42 -29
  31. package/dist/chunks/importer-WrGRZb0N.js.map +1 -0
  32. package/dist/chunks/{importer-BpuS3J8C.js → importer-xZ-3suY1.js} +74 -61
  33. package/dist/chunks/importer-xZ-3suY1.js.map +1 -0
  34. package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DKSqpNW1.js} +3 -3
  35. package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DKSqpNW1.js.map} +1 -1
  36. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-CtRXjg_i.js} +2 -2
  37. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-CtRXjg_i.js.map} +1 -1
  38. package/dist/chunks/{records-r3SFHz11.js → records-Cu4bxYOm.js} +69 -36
  39. package/dist/chunks/records-Cu4bxYOm.js.map +1 -0
  40. package/dist/chunks/{text-Bjfa1xYL.js → text-DHOJXnED.js} +20 -11
  41. package/dist/chunks/text-DHOJXnED.js.map +1 -0
  42. package/dist/csv.js +323 -241
  43. package/dist/csv.js.map +1 -1
  44. package/dist/cx.js +1 -1
  45. package/dist/cx2.js +2 -2
  46. package/dist/cys.js +1 -1
  47. package/dist/dot.js +1 -1
  48. package/dist/gexf.js +195 -97
  49. package/dist/gexf.js.map +1 -1
  50. package/dist/gml.js +170 -149
  51. package/dist/gml.js.map +1 -1
  52. package/dist/graph-io.js +27 -27
  53. package/dist/graphml.js +2 -2
  54. package/dist/json.js +1 -1
  55. package/dist/neo4j.js +103 -65
  56. package/dist/neo4j.js.map +1 -1
  57. package/dist/obo.js +1 -1
  58. package/dist/pajek.js +1 -1
  59. package/dist/src/children.d.ts.map +1 -1
  60. package/dist/src/children.js +50 -27
  61. package/dist/src/children.js.map +1 -1
  62. package/dist/src/common/attributes.d.ts.map +1 -1
  63. package/dist/src/common/attributes.js +86 -42
  64. package/dist/src/common/attributes.js.map +1 -1
  65. package/dist/src/common/input.d.ts.map +1 -1
  66. package/dist/src/common/input.js +358 -184
  67. package/dist/src/common/input.js.map +1 -1
  68. package/dist/src/common/lists.js +30 -15
  69. package/dist/src/common/lists.js.map +1 -1
  70. package/dist/src/common/text.d.ts +7 -0
  71. package/dist/src/common/text.d.ts.map +1 -1
  72. package/dist/src/common/text.js +15 -7
  73. package/dist/src/common/text.js.map +1 -1
  74. package/dist/src/common/zip.d.ts.map +1 -1
  75. package/dist/src/common/zip.js +36 -19
  76. package/dist/src/common/zip.js.map +1 -1
  77. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  78. package/dist/src/formats/csv/exporter.js +288 -146
  79. package/dist/src/formats/csv/exporter.js.map +1 -1
  80. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  81. package/dist/src/formats/csv/importer.js +129 -80
  82. package/dist/src/formats/csv/importer.js.map +1 -1
  83. package/dist/src/formats/csv/records.d.ts.map +1 -1
  84. package/dist/src/formats/csv/records.js +78 -36
  85. package/dist/src/formats/csv/records.js.map +1 -1
  86. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  87. package/dist/src/formats/cys/tables.js +11 -8
  88. package/dist/src/formats/cys/tables.js.map +1 -1
  89. package/dist/src/formats/dot/tokenizer.d.ts +39 -0
  90. package/dist/src/formats/dot/tokenizer.d.ts.map +1 -1
  91. package/dist/src/formats/dot/tokenizer.js +112 -66
  92. package/dist/src/formats/dot/tokenizer.js.map +1 -1
  93. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  94. package/dist/src/formats/gexf/exporter.js +65 -37
  95. package/dist/src/formats/gexf/exporter.js.map +1 -1
  96. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/importer.js +138 -50
  98. package/dist/src/formats/gexf/importer.js.map +1 -1
  99. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  100. package/dist/src/formats/gml/exporter.js +51 -35
  101. package/dist/src/formats/gml/exporter.js.map +1 -1
  102. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  103. package/dist/src/formats/gml/importer.js +103 -58
  104. package/dist/src/formats/gml/importer.js.map +1 -1
  105. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  106. package/dist/src/formats/gml/syntax.js +17 -8
  107. package/dist/src/formats/gml/syntax.js.map +1 -1
  108. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  109. package/dist/src/formats/graphml/exporter.js +34 -24
  110. package/dist/src/formats/graphml/exporter.js.map +1 -1
  111. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/importer.js +17 -13
  113. package/dist/src/formats/graphml/importer.js.map +1 -1
  114. package/dist/src/formats/graphml/tree.js +23 -14
  115. package/dist/src/formats/graphml/tree.js.map +1 -1
  116. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  117. package/dist/src/formats/graphml/yfiles.js +22 -10
  118. package/dist/src/formats/graphml/yfiles.js.map +1 -1
  119. package/dist/src/formats/json/dialect.d.ts.map +1 -1
  120. package/dist/src/formats/json/dialect.js +54 -20
  121. package/dist/src/formats/json/dialect.js.map +1 -1
  122. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  123. package/dist/src/formats/json/exporter.js +38 -21
  124. package/dist/src/formats/json/exporter.js.map +1 -1
  125. package/dist/src/formats/json/importer.d.ts.map +1 -1
  126. package/dist/src/formats/json/importer.js +536 -343
  127. package/dist/src/formats/json/importer.js.map +1 -1
  128. package/dist/src/formats/json/obographs.js +29 -17
  129. package/dist/src/formats/json/obographs.js.map +1 -1
  130. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  131. package/dist/src/formats/neo4j/exporter.js +73 -43
  132. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  133. package/dist/src/formats/neo4j/header.d.ts.map +1 -1
  134. package/dist/src/formats/neo4j/header.js +37 -16
  135. package/dist/src/formats/neo4j/header.js.map +1 -1
  136. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  137. package/dist/src/formats/neo4j/importer.js +21 -11
  138. package/dist/src/formats/neo4j/importer.js.map +1 -1
  139. package/dist/src/formats/obo/exporter.d.ts.map +1 -1
  140. package/dist/src/formats/obo/exporter.js +18 -7
  141. package/dist/src/formats/obo/exporter.js.map +1 -1
  142. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  143. package/dist/src/formats/pajek/exporter.js +142 -73
  144. package/dist/src/formats/pajek/exporter.js.map +1 -1
  145. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  146. package/dist/src/formats/pajek/importer.js +26 -15
  147. package/dist/src/formats/pajek/importer.js.map +1 -1
  148. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  149. package/dist/src/formats/pajek/syntax.js +85 -58
  150. package/dist/src/formats/pajek/syntax.js.map +1 -1
  151. package/dist/src/formats/xgmml/emit.d.ts +7 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +79 -40
  154. package/dist/src/formats/xgmml/emit.js.map +1 -1
  155. package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
  156. package/dist/src/formats/xgmml/exporter.js +38 -12
  157. package/dist/src/formats/xgmml/exporter.js.map +1 -1
  158. package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
  159. package/dist/src/formats/xgmml/importer.js +38 -18
  160. package/dist/src/formats/xgmml/importer.js.map +1 -1
  161. package/dist/src/sniff.js +109 -57
  162. package/dist/src/sniff.js.map +1 -1
  163. package/dist/xgmml.js +2 -2
  164. package/package.json +2 -2
  165. package/src/children.ts +54 -27
  166. package/src/common/attributes.ts +118 -49
  167. package/src/common/input.ts +439 -197
  168. package/src/common/lists.ts +29 -15
  169. package/src/common/text.ts +24 -15
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +377 -175
  172. package/src/formats/csv/importer.ts +155 -84
  173. package/src/formats/csv/records.ts +90 -35
  174. package/src/formats/cys/tables.ts +11 -10
  175. package/src/formats/dot/tokenizer.ts +118 -68
  176. package/src/formats/gexf/exporter.ts +67 -36
  177. package/src/formats/gexf/importer.ts +173 -63
  178. package/src/formats/gml/exporter.ts +78 -82
  179. package/src/formats/gml/importer.ts +119 -68
  180. package/src/formats/gml/syntax.ts +18 -8
  181. package/src/formats/graphml/exporter.ts +44 -24
  182. package/src/formats/graphml/importer.ts +18 -11
  183. package/src/formats/graphml/tree.ts +22 -12
  184. package/src/formats/graphml/yfiles.ts +23 -10
  185. package/src/formats/json/dialect.ts +61 -22
  186. package/src/formats/json/exporter.ts +40 -21
  187. package/src/formats/json/importer.ts +701 -385
  188. package/src/formats/json/obographs.ts +35 -15
  189. package/src/formats/neo4j/exporter.ts +79 -41
  190. package/src/formats/neo4j/header.ts +50 -16
  191. package/src/formats/neo4j/importer.ts +21 -10
  192. package/src/formats/obo/exporter.ts +34 -17
  193. package/src/formats/pajek/exporter.ts +180 -117
  194. package/src/formats/pajek/importer.ts +27 -15
  195. package/src/formats/pajek/syntax.ts +99 -58
  196. package/src/formats/xgmml/emit.ts +88 -40
  197. package/src/formats/xgmml/exporter.ts +40 -12
  198. package/src/formats/xgmml/importer.ts +45 -23
  199. package/src/sniff.ts +115 -59
  200. package/dist/chunks/children-CL3Cy0ez.js.map +0 -1
  201. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  202. package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
  203. package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
  204. package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
  205. package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
  206. package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
  207. package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
  208. package/dist/chunks/importer-DcKB97kW.js.map +0 -1
  209. package/dist/chunks/records-r3SFHz11.js.map +0 -1
  210. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -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
- // windows-1252 text holds no control bytes but TAB, LF, FF and CR: one anywhere in
510
- // undeclared input that is not UTF-8 marks binary data (watch() saw every byte so far)
511
- if (this.control !== null) {
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
- const total = onProgress === null ? 0 : utf8Length(input);
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
- const length = input.byteLength;
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 first bytes of a stream are held back until the BOM check (and the declaration check,
1295
- // when the format has one) can see enough of them, or the input ended, or text arrived, so a
1296
- // BOM or a declaration split across small chunks is still seen
1297
- const needed = options.declaredEncoding === undefined ? BOM_BYTES : headBytes;
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 head: Uint8Array[] = [];
1300
- let headLength = 0;
1301
- const flushHead = (stream: boolean): string => {
1302
- const bytes = concatBytes(head);
1303
- head = [];
1304
- headLength = 0;
1305
- decoder.start(bytes.subarray(0, headBytes));
1306
- return decoder.decode(bytes, stream);
1307
- };
1308
- const chunks = isReadableStream(input) ? streamChunks(input, signal) : input;
1309
- for await (const chunk of chunks) {
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
- let text: string;
1312
- const bytes = typeof chunk === "string" ? null : asBytes(chunk);
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
- // finish any byte sequence still pending in the decoder before switching to text; the
1315
- // BOM and declaration checks apply to the start of the input only, so bytes after text
1316
- // are never taken for a BOM
1317
- let pending = "";
1318
- if (headLength > 0) {
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
- if (text.length > 0) {
1343
- yield text;
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
- onProgress?.(done);
1476
+ return this.filter.push(tail);
1346
1477
  }
1347
- let tail = "";
1348
- if (headLength > 0) {
1349
- tail = flushHead(false);
1350
- } else if (decoder.started) {
1351
- tail = decoder.decode(new Uint8Array(0), false);
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
- tail = filter.push(tail);
1354
- if (tail.length > 0) {
1355
- yield tail;
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 (none of them holds a line break, except that the last
1552
- // may end with a `\r` whose meaning the next chunk decides); joined once when the line ends,
1553
- // so a line spanning many chunks costs its length, not its length times the chunk count.
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
- const joined = pending.join("");
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
- // the next `\n` and `\r` at or after `start`; each is searched for once per chunk and
1574
- // again only after it was consumed, so a chunk is scanned once whatever its line count
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
- const piece = chunk.slice(start, cut);
1593
- if (pending.length === 0) {
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 < end) {
1614
- pendingLength += end - start;
1615
- if (pendingLength > MAX_TEXT_LENGTH) {
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.length > 0) {
1727
+ if (!pending.empty) {
1623
1728
  this.lineNumber++;
1624
- const joined = pending.join("");
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
+ }