@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
@@ -405,31 +405,58 @@ class ByteDecoder {
405
405
  const bad = this.encoding === "utf-8" ? invalidUtf8(all) : null;
406
406
  const at = base + (bad === null ? 0 : bad.index);
407
407
  if (this.mayFallBack && this.asciiSoFar) {
408
- // windows-1252 text holds no control bytes but TAB, LF, FF and CR: one anywhere in
409
- // undeclared input that is not UTF-8 marks binary data (watch() saw every byte so far)
410
- if (this.control !== null) {
411
- return this.binary(this.control);
412
- }
413
- if (this.textFollows && bad?.cut === true) {
414
- return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`, undefined, { byteOffset: at });
415
- }
416
- if (!stream && bad?.truncated === true) {
417
- return this.report.fail(INVALID_UTF8_CODE, `the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`, undefined, { byteOffset: at });
418
- }
419
- if (!startsWithUtf8(all)) {
420
- const cutNote = !stream && bad?.cut === true
421
- ? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated"
422
- : "";
423
- this.report.warning("coercion", ENCODING_FALLBACK_CODE, this.declaredUtf8
424
- ? `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)`
425
- : `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)`);
426
- this.use("windows-1252", false);
427
- this.fellBack = true;
428
- this.mixedPending = true;
429
- this.reportUtf8After(all, base);
430
- return this.decoder.decode(all, { stream });
408
+ const text = this.fallBack(all, bad, at, base, stream);
409
+ if (text !== null) {
410
+ return text;
431
411
  }
432
412
  }
413
+ return this.failDecode(bytes, at, base, stream);
414
+ }
415
+ /**
416
+ * Undeclared input that was ASCII so far failed to decode: fail on binary data or a cut
417
+ * sequence, else switch to windows-1252 with a warning unless the bytes start with valid
418
+ * non-ASCII UTF-8.
419
+ * @param all - the held-back bytes followed by the bytes that failed
420
+ * @param bad - the first invalid UTF-8 sequence, or null when the encoding is not UTF-8
421
+ * @param at - the byte offset of the first invalid byte
422
+ * @param base - the byte offset of `all`
423
+ * @param stream - whether more bytes follow
424
+ * @returns the bytes decoded as windows-1252, or null when they do not fall back
425
+ */
426
+ fallBack(all, bad, at, base, stream) {
427
+ // windows-1252 text holds no control bytes but TAB, LF, FF and CR: one anywhere in
428
+ // undeclared input that is not UTF-8 marks binary data (watch() saw every byte so far)
429
+ if (this.control !== null) {
430
+ return this.binary(this.control);
431
+ }
432
+ if (this.textFollows && bad?.cut === true) {
433
+ return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}: a byte chunk ends inside a multi-byte sequence and a text chunk follows`, undefined, { byteOffset: at });
434
+ }
435
+ if (!stream && bad?.truncated === true) {
436
+ return this.report.fail(INVALID_UTF8_CODE, `the input ends in the middle of a UTF-8 sequence at byte ${at} (it may be truncated)`, undefined, { byteOffset: at });
437
+ }
438
+ if (startsWithUtf8(all)) {
439
+ return null;
440
+ }
441
+ const cutNote = !stream && bad?.cut === true ? ": it ends inside a multi-byte UTF-8 sequence, so it may be truncated" : "";
442
+ this.report.warning("coercion", ENCODING_FALLBACK_CODE, this.declaredUtf8
443
+ ? `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)`
444
+ : `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)`);
445
+ this.use("windows-1252", false);
446
+ this.fellBack = true;
447
+ this.mixedPending = true;
448
+ this.reportUtf8After(all, base);
449
+ return this.decoder.decode(all, { stream });
450
+ }
451
+ /**
452
+ * Fail the import on bytes that are not valid in the encoding.
453
+ * @param bytes - the bytes that failed
454
+ * @param at - the byte offset of the first invalid UTF-8 byte
455
+ * @param base - the byte offset of the held-back bytes and these
456
+ * @param stream - whether more bytes follow
457
+ * @returns never: it throws
458
+ */
459
+ failDecode(bytes, at, base, stream) {
433
460
  if (this.encoding === "utf-8") {
434
461
  const after = this.mayFallBack ? " after valid non-ASCII UTF-8 text; pass the encoding option" : "";
435
462
  return this.report.fail(INVALID_UTF8_CODE, `invalid UTF-8 at byte ${at}${after}`, undefined, {
@@ -1018,132 +1045,210 @@ function reportTextEncoding(report, encoding) {
1018
1045
  export async function* textChunks(rawInput, report, options = {}) {
1019
1046
  const input = normalizeInput(rawInput);
1020
1047
  const signal = options.signal ?? null;
1021
- const onProgress = options.onProgress ?? null;
1022
1048
  const filter = new TextFilter(report, options);
1023
1049
  throwIfAborted(signal);
1024
1050
  if (typeof input === "string") {
1025
1051
  reportTextEncoding(report, options.encoding);
1026
- const total = onProgress === null ? 0 : utf8Length(input);
1027
- let done = 0;
1028
- let start = 0;
1029
- while (start < input.length) {
1030
- throwIfAborted(signal);
1031
- let end = Math.min(start + DECODE_SLICE, input.length);
1032
- const highSurrogate = (input.charCodeAt(end - 1) & 0xfc00) === 0xd800; // NOSONAR(S7758): reads UTF-16 code units on purpose
1033
- if (end < input.length && highSurrogate) {
1034
- end++; // never split a surrogate pair
1035
- }
1036
- const piece = start === 0 && end === input.length ? input : input.slice(start, end);
1037
- start = end;
1038
- const text = filter.push(piece);
1039
- if (text.length > 0) {
1040
- yield text;
1041
- }
1042
- if (onProgress !== null) {
1043
- done += utf8Length(piece);
1044
- onProgress(done, total);
1045
- }
1046
- }
1047
- if (input.length === 0) {
1048
- onProgress?.(0, 0);
1049
- }
1050
- filter.end();
1052
+ yield* stringTextChunks(input, filter, options);
1051
1053
  return;
1052
1054
  }
1053
1055
  const decoder = new ByteDecoder(report, options);
1054
- const headBytes = Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
1055
1056
  if (input instanceof Uint8Array) {
1056
- const length = input.byteLength;
1057
- decoder.start(input.subarray(0, headBytes));
1058
- for (let offset = 0; offset < length; offset += DECODE_SLICE) {
1059
- throwIfAborted(signal);
1060
- if (input.byteLength !== length) {
1061
- throw new GraphFormatError("E_UNSUPPORTED", "the input's buffer was detached (transferred) or resized during the import", { reason: "detached" });
1062
- }
1063
- const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
1064
- const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
1065
- if (text.length > 0) {
1066
- yield text;
1067
- }
1068
- onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
1069
- }
1070
- if (length === 0) {
1071
- onProgress?.(0, 0);
1072
- }
1073
- filter.end();
1057
+ yield* byteTextChunks(input, filter, decoder, options);
1074
1058
  return;
1075
1059
  }
1076
- // the first bytes of a stream are held back until the BOM check (and the declaration check,
1077
- // when the format has one) can see enough of them, or the input ended, or text arrived, so a
1078
- // BOM or a declaration split across small chunks is still seen
1079
- const needed = options.declaredEncoding === undefined ? BOM_BYTES : headBytes;
1060
+ // the stream's loop stays here rather than in a delegated generator: a yield* layer costs
1061
+ // time on every chunk, and a stream may arrive in many small ones
1062
+ const stream = new StreamText(filter, decoder, options);
1063
+ for await (const chunk of isReadableStream(input) ? streamChunks(input, signal) : input) {
1064
+ throwIfAborted(signal);
1065
+ const text = stream.push(chunk);
1066
+ if (text.length > 0) {
1067
+ yield text;
1068
+ }
1069
+ options.onProgress?.(stream.done);
1070
+ }
1071
+ const tail = stream.end();
1072
+ if (tail.length > 0) {
1073
+ yield tail;
1074
+ }
1075
+ filter.end();
1076
+ options.onProgress?.(stream.done, stream.done);
1077
+ }
1078
+ /**
1079
+ * The end of the slice of a string input that starts at start: DECODE_SLICE code units, one more
1080
+ * when the slice would end between the two halves of a surrogate pair.
1081
+ * @param input - the string
1082
+ * @param start - where the slice starts
1083
+ * @returns where it ends
1084
+ */
1085
+ function stringSliceEnd(input, start) {
1086
+ const end = Math.min(start + DECODE_SLICE, input.length);
1087
+ const highSurrogate = (input.charCodeAt(end - 1) & 0xfc00) === 0xd800; // NOSONAR(S7758): reads UTF-16 code units on purpose
1088
+ return end < input.length && highSurrogate ? end + 1 : end;
1089
+ }
1090
+ /**
1091
+ * textChunks() of a string input: slices of it, through the text filter.
1092
+ * @param input - the string
1093
+ * @param filter - the text checks
1094
+ * @param options - cancellation and progress
1095
+ * @yields the filtered slices
1096
+ * @returns nothing
1097
+ */
1098
+ function* stringTextChunks(input, filter, options) {
1099
+ const signal = options.signal ?? null;
1100
+ const onProgress = options.onProgress ?? null;
1101
+ const total = onProgress === null ? 0 : utf8Length(input);
1080
1102
  let done = 0;
1081
- let head = [];
1082
- let headLength = 0;
1083
- const flushHead = (stream) => {
1084
- const bytes = concatBytes(head);
1085
- head = [];
1086
- headLength = 0;
1087
- decoder.start(bytes.subarray(0, headBytes));
1088
- return decoder.decode(bytes, stream);
1089
- };
1090
- const chunks = isReadableStream(input) ? streamChunks(input, signal) : input;
1091
- for await (const chunk of chunks) {
1103
+ let start = 0;
1104
+ while (start < input.length) {
1092
1105
  throwIfAborted(signal);
1093
- let text;
1094
- const bytes = typeof chunk === "string" ? null : asBytes(chunk);
1095
- if (typeof chunk === "string") {
1096
- // finish any byte sequence still pending in the decoder before switching to text; the
1097
- // BOM and declaration checks apply to the start of the input only, so bytes after text
1098
- // are never taken for a BOM
1099
- let pending = "";
1100
- if (headLength > 0) {
1101
- pending = decoder.flushBeforeText(() => flushHead(false));
1102
- }
1103
- else if (decoder.started) {
1104
- pending = decoder.flushBeforeText(() => decoder.decode(new Uint8Array(0), false));
1105
- }
1106
- else {
1107
- decoder.start(new Uint8Array(0));
1108
- }
1109
- text = filter.push(pending + chunk);
1110
- done += onProgress === null ? 0 : utf8Length(chunk);
1106
+ const end = stringSliceEnd(input, start);
1107
+ const piece = start === 0 && end === input.length ? input : input.slice(start, end);
1108
+ start = end;
1109
+ const text = filter.push(piece);
1110
+ if (text.length > 0) {
1111
+ yield text;
1111
1112
  }
1112
- else if (bytes !== null) {
1113
- done += bytes.byteLength;
1114
- if (decoder.started) {
1115
- text = filter.push(decoder.decode(bytes, true));
1116
- }
1117
- else {
1118
- head.push(bytes);
1119
- headLength += bytes.byteLength;
1120
- text = headLength >= needed ? filter.push(flushHead(true)) : "";
1121
- }
1113
+ if (onProgress !== null) {
1114
+ done += utf8Length(piece);
1115
+ onProgress(done, total);
1122
1116
  }
1123
- else {
1117
+ }
1118
+ if (input.length === 0) {
1119
+ onProgress?.(0, 0);
1120
+ }
1121
+ filter.end();
1122
+ }
1123
+ /**
1124
+ * textChunks() of a Uint8Array input: slices of it, decoded, through the text filter.
1125
+ * @param input - the bytes
1126
+ * @param filter - the text checks
1127
+ * @param decoder - the byte decoder
1128
+ * @param options - cancellation, progress and the declaration size
1129
+ * @yields the decoded, filtered slices
1130
+ * @returns nothing
1131
+ */
1132
+ function* byteTextChunks(input, filter, decoder, options) {
1133
+ const signal = options.signal ?? null;
1134
+ const length = input.byteLength;
1135
+ decoder.start(input.subarray(0, headByteCount(options)));
1136
+ for (let offset = 0; offset < length; offset += DECODE_SLICE) {
1137
+ throwIfAborted(signal);
1138
+ if (input.byteLength !== length) {
1139
+ throw new GraphFormatError("E_UNSUPPORTED", "the input's buffer was detached (transferred) or resized during the import", { reason: "detached" });
1140
+ }
1141
+ const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
1142
+ const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
1143
+ if (text.length > 0) {
1144
+ yield text;
1145
+ }
1146
+ options.onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
1147
+ }
1148
+ if (length === 0) {
1149
+ options.onProgress?.(0, 0);
1150
+ }
1151
+ filter.end();
1152
+ }
1153
+ /**
1154
+ * How many leading bytes the encoding is chosen from: the format's declaration size, at least a BOM.
1155
+ * @param options - the declaration size
1156
+ * @returns the byte count
1157
+ */
1158
+ function headByteCount(options) {
1159
+ return Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
1160
+ }
1161
+ /**
1162
+ * The chunks of a stream input (a ReadableStream or an async iterable of strings and bytes) turned
1163
+ * into filtered text. The first bytes are held back until the BOM check (and the declaration
1164
+ * check, when the format has one) can see enough of them, or the input ended, or text arrived, so
1165
+ * a BOM or a declaration split across small chunks is still seen.
1166
+ */
1167
+ class StreamText {
1168
+ /**
1169
+ * Create the reader of one stream input.
1170
+ * @param filter - the text checks
1171
+ * @param decoder - the byte decoder
1172
+ * @param options - progress, the declaration reader and its size
1173
+ */
1174
+ constructor(filter, decoder, options) {
1175
+ this.filter = filter;
1176
+ this.decoder = decoder;
1177
+ /** Bytes read so far (text counted as UTF-8 when progress is reported), for progress. */
1178
+ this.done = 0;
1179
+ this.head = [];
1180
+ this.headLength = 0;
1181
+ this.headBytes = headByteCount(options);
1182
+ this.needed = options.declaredEncoding === undefined ? BOM_BYTES : this.headBytes;
1183
+ this.counting = (options.onProgress ?? null) !== null;
1184
+ }
1185
+ /**
1186
+ * Read the next chunk.
1187
+ * @param chunk - a string or a Uint8Array (anything else is E_UNSUPPORTED)
1188
+ * @returns its filtered text; empty while bytes are held back
1189
+ */
1190
+ push(chunk) {
1191
+ if (typeof chunk === "string") {
1192
+ this.done += this.counting ? utf8Length(chunk) : 0;
1193
+ return this.filter.push(this.beforeText() + chunk);
1194
+ }
1195
+ const bytes = asBytes(chunk);
1196
+ if (bytes === null) {
1124
1197
  throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
1125
1198
  reason: "chunk type",
1126
1199
  found: typeof chunk,
1127
1200
  });
1128
1201
  }
1129
- if (text.length > 0) {
1130
- yield text;
1202
+ this.done += bytes.byteLength;
1203
+ if (this.decoder.started) {
1204
+ return this.filter.push(this.decoder.decode(bytes, true));
1131
1205
  }
1132
- onProgress?.(done);
1206
+ this.head.push(bytes);
1207
+ this.headLength += bytes.byteLength;
1208
+ return this.headLength >= this.needed ? this.filter.push(this.flushHead(true)) : "";
1133
1209
  }
1134
- let tail = "";
1135
- if (headLength > 0) {
1136
- tail = flushHead(false);
1210
+ /**
1211
+ * The input ended: the filtered text of whatever is still held or pending.
1212
+ * @returns the text
1213
+ */
1214
+ end() {
1215
+ let tail = "";
1216
+ if (this.headLength > 0) {
1217
+ tail = this.flushHead(false);
1218
+ }
1219
+ else if (this.decoder.started) {
1220
+ tail = this.decoder.decode(new Uint8Array(0), false);
1221
+ }
1222
+ return this.filter.push(tail);
1137
1223
  }
1138
- else if (decoder.started) {
1139
- tail = decoder.decode(new Uint8Array(0), false);
1224
+ /**
1225
+ * Start the decoder on the held bytes and decode them.
1226
+ * @param stream - whether more bytes follow
1227
+ * @returns their text
1228
+ */
1229
+ flushHead(stream) {
1230
+ const bytes = concatBytes(this.head);
1231
+ this.head = [];
1232
+ this.headLength = 0;
1233
+ this.decoder.start(bytes.subarray(0, this.headBytes));
1234
+ return this.decoder.decode(bytes, stream);
1140
1235
  }
1141
- tail = filter.push(tail);
1142
- if (tail.length > 0) {
1143
- yield tail;
1236
+ /**
1237
+ * A text chunk follows: finish any byte sequence still pending in the decoder before switching
1238
+ * to text. The BOM and declaration checks apply to the start of the input only, so bytes after
1239
+ * text are never taken for a BOM.
1240
+ * @returns the text of the pending bytes
1241
+ */
1242
+ beforeText() {
1243
+ if (this.headLength > 0) {
1244
+ return this.decoder.flushBeforeText(() => this.flushHead(false));
1245
+ }
1246
+ if (this.decoder.started) {
1247
+ return this.decoder.flushBeforeText(() => this.decoder.decode(new Uint8Array(0), false));
1248
+ }
1249
+ this.decoder.start(new Uint8Array(0));
1250
+ return "";
1144
1251
  }
1145
- filter.end();
1146
- onProgress?.(done, done);
1147
1252
  }
1148
1253
  /**
1149
1254
  * Whether an input is a ReadableStream (by duck type, so a stream from another realm qualifies).
@@ -1309,84 +1414,153 @@ export class LineReader {
1309
1414
  * @returns nothing
1310
1415
  */
1311
1416
  async *[Symbol.asyncIterator]() {
1312
- // The pieces of the line in progress (none of them holds a line break, except that the last
1313
- // may end with a `\r` whose meaning the next chunk decides); joined once when the line ends,
1314
- // so a line spanning many chunks costs its length, not its length times the chunk count.
1315
- const pending = [];
1316
- let pendingLength = 0;
1417
+ // The pieces of the line in progress, joined once when the line ends, so a line spanning
1418
+ // many chunks costs its length, not its length times the chunk count.
1419
+ const pending = new PendingLine((line) => tooLarge(this.report, this.lineTooLong(), line));
1317
1420
  let trailingCr = false;
1318
1421
  for await (const chunk of textChunks(this.input, this.report, this.options)) {
1319
1422
  let start = 0;
1320
- const end = chunk.length;
1321
1423
  if (trailingCr) {
1322
1424
  // the previous chunk ended with `\r`: that ended a line, and a leading `\n` here is
1323
1425
  // the second half of the same terminator
1324
1426
  trailingCr = false;
1325
1427
  this.lineNumber++;
1326
- const joined = pending.join("");
1327
- pending.length = 0;
1328
- pendingLength = 0;
1329
- yield joined.slice(0, -1);
1428
+ yield pending.take().slice(0, -1);
1330
1429
  if (chunk.charCodeAt(0) === 10) {
1331
1430
  start = 1;
1332
1431
  }
1333
1432
  }
1334
- // the next `\n` and `\r` at or after `start`; each is searched for once per chunk and
1335
- // again only after it was consumed, so a chunk is scanned once whatever its line count
1336
- let nl = chunk.indexOf("\n", start);
1337
- let cr = chunk.indexOf("\r", start);
1338
- while (nl >= 0 || cr >= 0) {
1339
- let cut;
1340
- let next;
1341
- if (cr >= 0 && (nl < 0 || cr < nl)) {
1342
- if (cr === end - 1) {
1343
- // a trailing \r may be the first half of \r\n split across chunks
1344
- break;
1345
- }
1346
- cut = cr;
1347
- next = chunk.charCodeAt(cr + 1) === 10 ? cr + 2 : cr + 1;
1348
- }
1349
- else {
1350
- cut = nl;
1351
- next = nl + 1;
1352
- }
1433
+ const breaks = new LineBreaks(chunk, start);
1434
+ while (breaks.advance()) {
1353
1435
  this.lineNumber++;
1354
- const piece = chunk.slice(start, cut);
1355
- if (pending.length === 0) {
1356
- yield piece;
1357
- }
1358
- else {
1359
- if (pendingLength + piece.length > MAX_TEXT_LENGTH) {
1360
- tooLarge(this.report, this.lineTooLong(), this.lineNumber);
1361
- }
1362
- pending.push(piece);
1363
- const joined = pending.join("");
1364
- pending.length = 0;
1365
- pendingLength = 0;
1366
- yield joined;
1367
- }
1368
- start = next;
1369
- if (nl >= 0 && nl < next) {
1370
- nl = chunk.indexOf("\n", next);
1371
- }
1372
- if (cr >= 0 && cr < next) {
1373
- cr = chunk.indexOf("\r", next);
1374
- }
1436
+ yield pending.finish(chunk.slice(start, breaks.cut), this.lineNumber);
1437
+ start = breaks.next;
1375
1438
  }
1376
- if (start < end) {
1377
- pendingLength += end - start;
1378
- if (pendingLength > MAX_TEXT_LENGTH) {
1379
- tooLarge(this.report, this.lineTooLong(), this.lineNumber + 1);
1380
- }
1381
- pending.push(start === 0 ? chunk : chunk.slice(start));
1382
- trailingCr = chunk.charCodeAt(end - 1) === 13;
1439
+ if (start < chunk.length) {
1440
+ pending.appendRest(chunk, start, this.lineNumber + 1);
1441
+ trailingCr = chunk.codePointAt(chunk.length - 1) === 13;
1383
1442
  }
1384
1443
  }
1385
- if (pending.length > 0) {
1444
+ if (!pending.empty) {
1386
1445
  this.lineNumber++;
1387
- const joined = pending.join("");
1446
+ const joined = pending.take();
1388
1447
  yield joined.endsWith("\r") ? joined.slice(0, -1) : joined;
1389
1448
  }
1390
1449
  }
1391
1450
  }
1451
+ /**
1452
+ * The pieces of a line that spans chunks. None of them holds a line break, except that the last
1453
+ * may end with a `\r` whose meaning the next chunk decides.
1454
+ */
1455
+ class PendingLine {
1456
+ /**
1457
+ * Start an empty line.
1458
+ * @param tooLong - fails the import when the line grows longer than one string can hold
1459
+ */
1460
+ constructor(tooLong) {
1461
+ this.pieces = [];
1462
+ this.length = 0;
1463
+ this.tooLong = tooLong;
1464
+ }
1465
+ /**
1466
+ * Whether no piece is held.
1467
+ * @returns true when the line in progress is empty
1468
+ */
1469
+ get empty() {
1470
+ return this.pieces.length === 0;
1471
+ }
1472
+ /**
1473
+ * Hold the rest of a chunk from `start`: the line goes on in the next chunk.
1474
+ * @param chunk - the chunk
1475
+ * @param start - where the unfinished line starts in it
1476
+ * @param line - the 1-based number of the line, for the too-long error
1477
+ */
1478
+ appendRest(chunk, start, line) {
1479
+ this.append(start === 0 ? chunk : chunk.slice(start), line);
1480
+ }
1481
+ /**
1482
+ * End the line with its last piece.
1483
+ * @param piece - the text from the start of the chunk (or the last break) to the line break
1484
+ * @param line - the 1-based number of the line, for the too-long error
1485
+ * @returns the whole line
1486
+ */
1487
+ finish(piece, line) {
1488
+ if (this.pieces.length === 0) {
1489
+ return piece;
1490
+ }
1491
+ this.append(piece, line);
1492
+ return this.take();
1493
+ }
1494
+ /**
1495
+ * Join the held pieces and start a new line.
1496
+ * @returns the joined text
1497
+ */
1498
+ take() {
1499
+ const joined = this.pieces.join("");
1500
+ this.pieces.length = 0;
1501
+ this.length = 0;
1502
+ return joined;
1503
+ }
1504
+ /**
1505
+ * Hold one more piece.
1506
+ * @param piece - the piece
1507
+ * @param line - the 1-based number of the line, for the too-long error
1508
+ */
1509
+ append(piece, line) {
1510
+ this.length += piece.length;
1511
+ if (this.length > MAX_TEXT_LENGTH) {
1512
+ this.tooLong(line);
1513
+ }
1514
+ this.pieces.push(piece);
1515
+ }
1516
+ }
1517
+ /**
1518
+ * The line breaks of one chunk, in order: `\n`, `\r\n` and a lone `\r`. Each of `\n` and `\r` is
1519
+ * searched for once per chunk and again only after it was consumed, so a chunk is scanned once
1520
+ * whatever its line count.
1521
+ */
1522
+ class LineBreaks {
1523
+ /**
1524
+ * Scan a chunk from a position.
1525
+ * @param chunk - the chunk
1526
+ * @param start - where scanning starts
1527
+ */
1528
+ constructor(chunk, start) {
1529
+ /** Where the line just found ends (its terminator starts there). */
1530
+ this.cut = 0;
1531
+ this.chunk = chunk;
1532
+ this.next = start;
1533
+ this.nl = chunk.indexOf("\n", start);
1534
+ this.cr = chunk.indexOf("\r", start);
1535
+ }
1536
+ /**
1537
+ * Find the next line break.
1538
+ * @returns false when none is left, or the only one left is a trailing `\r` (it may be the first
1539
+ * half of a `\r\n` split across chunks)
1540
+ */
1541
+ advance() {
1542
+ const { chunk, next } = this;
1543
+ if (this.nl >= 0 && this.nl < next) {
1544
+ this.nl = chunk.indexOf("\n", next);
1545
+ }
1546
+ if (this.cr >= 0 && this.cr < next) {
1547
+ this.cr = chunk.indexOf("\r", next);
1548
+ }
1549
+ const { nl, cr } = this;
1550
+ if (cr >= 0 && (nl < 0 || cr < nl)) {
1551
+ if (cr === chunk.length - 1) {
1552
+ return false;
1553
+ }
1554
+ this.cut = cr;
1555
+ this.next = chunk.codePointAt(cr + 1) === 10 ? cr + 2 : cr + 1;
1556
+ return true;
1557
+ }
1558
+ if (nl < 0) {
1559
+ return false;
1560
+ }
1561
+ this.cut = nl;
1562
+ this.next = nl + 1;
1563
+ return true;
1564
+ }
1565
+ }
1392
1566
  //# sourceMappingURL=input.js.map