@graphty/graph-io 0.3.39 → 0.3.41

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 (213) hide show
  1. package/dist/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
  2. package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
  3. package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
  4. package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
  5. package/dist/chunks/{escape-BpYMQZVa.js → escape-DBb8zuNE.js} +4 -3
  6. package/dist/chunks/escape-DBb8zuNE.js.map +1 -0
  7. package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
  8. package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
  9. package/dist/chunks/{exporter-CNJZF_BN.js → exporter-BqtmlxNr.js} +545 -459
  10. package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
  11. package/dist/chunks/{exporter-BqKs1Q12.js → exporter-CS35R6Fg.js} +6 -5
  12. package/dist/chunks/exporter-CS35R6Fg.js.map +1 -0
  13. package/dist/chunks/{exporter-DXqCPNrI.js → exporter-D8r1N2dU.js} +67 -42
  14. package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
  15. package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
  16. package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
  17. package/dist/chunks/{importer-CSt-gaYE.js → importer-BKC7AmCA.js} +42 -29
  18. package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
  19. package/dist/chunks/{importer-DvFznt5-.js → importer-BdWHmlmG.js} +112 -62
  20. package/dist/chunks/importer-BdWHmlmG.js.map +1 -0
  21. package/dist/chunks/{importer-CqgYq4Ld.js → importer-BfbzWk6k.js} +202 -181
  22. package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
  23. package/dist/chunks/{importer-PVXkeoaS.js → importer-BmaZcskq.js} +79 -66
  24. package/dist/chunks/importer-BmaZcskq.js.map +1 -0
  25. package/dist/chunks/{importer-B7Ir269L.js → importer-DviEsHy4.js} +118 -72
  26. package/dist/chunks/importer-DviEsHy4.js.map +1 -0
  27. package/dist/chunks/{importer--tQQsY4W.js → importer-S-abxJHn.js} +5 -5
  28. package/dist/chunks/{importer--tQQsY4W.js.map → importer-S-abxJHn.js.map} +1 -1
  29. package/dist/chunks/{importer-BfJ1jnNr.js → importer-r29cWnEf.js} +28 -13
  30. package/dist/chunks/importer-r29cWnEf.js.map +1 -0
  31. package/dist/chunks/{json-elements-CXs4z1iL.js → json-elements-DdF-etPT.js} +7 -5
  32. package/dist/chunks/{json-elements-CXs4z1iL.js.map → json-elements-DdF-etPT.js.map} +1 -1
  33. package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
  34. package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
  35. package/dist/chunks/{records-C9jd7yLD.js → records-Uc3LkABx.js} +150 -106
  36. package/dist/chunks/records-Uc3LkABx.js.map +1 -0
  37. package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
  38. package/dist/chunks/text-Bh9SaAob.js.map +1 -0
  39. package/dist/csv.js +67 -49
  40. package/dist/csv.js.map +1 -1
  41. package/dist/cx.js +1 -1
  42. package/dist/cx2.js +2 -2
  43. package/dist/cys.js +1 -1
  44. package/dist/dot.js +1 -1
  45. package/dist/gexf.js +199 -99
  46. package/dist/gexf.js.map +1 -1
  47. package/dist/gml.js +170 -149
  48. package/dist/gml.js.map +1 -1
  49. package/dist/graph-io.js +26 -26
  50. package/dist/graphml.js +2 -2
  51. package/dist/json.js +1 -1
  52. package/dist/neo4j.js +103 -65
  53. package/dist/neo4j.js.map +1 -1
  54. package/dist/obo.js +1 -1
  55. package/dist/pajek.js +1 -1
  56. package/dist/src/common/attributes.d.ts.map +1 -1
  57. package/dist/src/common/attributes.js +34 -9
  58. package/dist/src/common/attributes.js.map +1 -1
  59. package/dist/src/common/input.d.ts.map +1 -1
  60. package/dist/src/common/input.js +180 -102
  61. package/dist/src/common/input.js.map +1 -1
  62. package/dist/src/common/json-elements.d.ts.map +1 -1
  63. package/dist/src/common/json-elements.js +4 -2
  64. package/dist/src/common/json-elements.js.map +1 -1
  65. package/dist/src/common/text.d.ts +7 -0
  66. package/dist/src/common/text.d.ts.map +1 -1
  67. package/dist/src/common/text.js +15 -7
  68. package/dist/src/common/text.js.map +1 -1
  69. package/dist/src/common/xml.d.ts.map +1 -1
  70. package/dist/src/common/xml.js +2 -1
  71. package/dist/src/common/xml.js.map +1 -1
  72. package/dist/src/common/zip.d.ts.map +1 -1
  73. package/dist/src/common/zip.js +36 -19
  74. package/dist/src/common/zip.js.map +1 -1
  75. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  76. package/dist/src/formats/csv/exporter.js +77 -25
  77. package/dist/src/formats/csv/exporter.js.map +1 -1
  78. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  79. package/dist/src/formats/csv/importer.js +16 -6
  80. package/dist/src/formats/csv/importer.js.map +1 -1
  81. package/dist/src/formats/csv/records.d.ts.map +1 -1
  82. package/dist/src/formats/csv/records.js +156 -110
  83. package/dist/src/formats/csv/records.js.map +1 -1
  84. package/dist/src/formats/cx/importer.d.ts.map +1 -1
  85. package/dist/src/formats/cx/importer.js +108 -53
  86. package/dist/src/formats/cx/importer.js.map +1 -1
  87. package/dist/src/formats/cx2/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/cx2/exporter.js +2 -1
  89. package/dist/src/formats/cx2/exporter.js.map +1 -1
  90. package/dist/src/formats/cx2/importer.d.ts.map +1 -1
  91. package/dist/src/formats/cx2/importer.js +28 -6
  92. package/dist/src/formats/cx2/importer.js.map +1 -1
  93. package/dist/src/formats/cys/tables.d.ts.map +1 -1
  94. package/dist/src/formats/cys/tables.js +11 -8
  95. package/dist/src/formats/cys/tables.js.map +1 -1
  96. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  97. package/dist/src/formats/gexf/exporter.js +69 -39
  98. package/dist/src/formats/gexf/exporter.js.map +1 -1
  99. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  100. package/dist/src/formats/gexf/importer.js +138 -50
  101. package/dist/src/formats/gexf/importer.js.map +1 -1
  102. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  103. package/dist/src/formats/gml/exporter.js +51 -35
  104. package/dist/src/formats/gml/exporter.js.map +1 -1
  105. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  106. package/dist/src/formats/gml/importer.js +103 -58
  107. package/dist/src/formats/gml/importer.js.map +1 -1
  108. package/dist/src/formats/gml/syntax.d.ts.map +1 -1
  109. package/dist/src/formats/gml/syntax.js +17 -8
  110. package/dist/src/formats/gml/syntax.js.map +1 -1
  111. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  112. package/dist/src/formats/graphml/exporter.js +36 -25
  113. package/dist/src/formats/graphml/exporter.js.map +1 -1
  114. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  115. package/dist/src/formats/graphml/importer.js +20 -15
  116. package/dist/src/formats/graphml/importer.js.map +1 -1
  117. package/dist/src/formats/graphml/tree.js +23 -14
  118. package/dist/src/formats/graphml/tree.js.map +1 -1
  119. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
  120. package/dist/src/formats/graphml/yfiles.js +22 -10
  121. package/dist/src/formats/graphml/yfiles.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 +13 -0
  152. package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
  153. package/dist/src/formats/xgmml/emit.js +112 -50
  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 +92 -34
  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/common/attributes.ts +59 -19
  166. package/src/common/input.ts +216 -102
  167. package/src/common/json-elements.ts +4 -2
  168. package/src/common/text.ts +24 -15
  169. package/src/common/xml.ts +2 -1
  170. package/src/common/zip.ts +47 -18
  171. package/src/formats/csv/exporter.ts +103 -39
  172. package/src/formats/csv/importer.ts +17 -6
  173. package/src/formats/csv/records.ts +162 -109
  174. package/src/formats/cx/importer.ts +140 -58
  175. package/src/formats/cx2/exporter.ts +2 -1
  176. package/src/formats/cx2/importer.ts +34 -10
  177. package/src/formats/cys/tables.ts +11 -10
  178. package/src/formats/gexf/exporter.ts +71 -38
  179. package/src/formats/gexf/importer.ts +173 -63
  180. package/src/formats/gml/exporter.ts +78 -82
  181. package/src/formats/gml/importer.ts +119 -68
  182. package/src/formats/gml/syntax.ts +18 -8
  183. package/src/formats/graphml/exporter.ts +46 -27
  184. package/src/formats/graphml/importer.ts +21 -13
  185. package/src/formats/graphml/tree.ts +22 -12
  186. package/src/formats/graphml/yfiles.ts +23 -10
  187. package/src/formats/json/exporter.ts +40 -21
  188. package/src/formats/json/importer.ts +701 -385
  189. package/src/formats/json/obographs.ts +35 -15
  190. package/src/formats/neo4j/exporter.ts +79 -41
  191. package/src/formats/neo4j/header.ts +50 -16
  192. package/src/formats/neo4j/importer.ts +21 -10
  193. package/src/formats/obo/exporter.ts +34 -17
  194. package/src/formats/pajek/exporter.ts +180 -117
  195. package/src/formats/pajek/importer.ts +27 -15
  196. package/src/formats/pajek/syntax.ts +99 -58
  197. package/src/formats/xgmml/emit.ts +122 -50
  198. package/src/formats/xgmml/exporter.ts +103 -35
  199. package/src/formats/xgmml/importer.ts +45 -23
  200. package/src/sniff.ts +115 -59
  201. package/dist/chunks/escape-BpYMQZVa.js.map +0 -1
  202. package/dist/chunks/export-B-aVCwyG.js.map +0 -1
  203. package/dist/chunks/exporter-BqKs1Q12.js.map +0 -1
  204. package/dist/chunks/exporter-CNJZF_BN.js.map +0 -1
  205. package/dist/chunks/exporter-DXqCPNrI.js.map +0 -1
  206. package/dist/chunks/importer-B7Ir269L.js.map +0 -1
  207. package/dist/chunks/importer-BfJ1jnNr.js.map +0 -1
  208. package/dist/chunks/importer-CSt-gaYE.js.map +0 -1
  209. package/dist/chunks/importer-CqgYq4Ld.js.map +0 -1
  210. package/dist/chunks/importer-DvFznt5-.js.map +0 -1
  211. package/dist/chunks/importer-PVXkeoaS.js.map +0 -1
  212. package/dist/chunks/records-C9jd7yLD.js.map +0 -1
  213. package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
@@ -1232,130 +1232,244 @@ export async function* textChunks(
1232
1232
  ): AsyncGenerator<string, void, undefined> {
1233
1233
  const input = normalizeInput(rawInput);
1234
1234
  const signal = options.signal ?? null;
1235
- const onProgress = options.onProgress ?? null;
1236
1235
  const filter = new TextFilter(report, options);
1237
1236
  throwIfAborted(signal);
1238
1237
  if (typeof input === "string") {
1239
1238
  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();
1239
+ yield* stringTextChunks(input, filter, options);
1265
1240
  return;
1266
1241
  }
1267
1242
  const decoder = new ByteDecoder(report, options);
1268
- const headBytes = Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
1269
1243
  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();
1244
+ yield* byteTextChunks(input, filter, decoder, options);
1292
1245
  return;
1293
1246
  }
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;
1247
+ // the stream's loop stays here rather than in a delegated generator: a yield* layer costs
1248
+ // time on every chunk, and a stream may arrive in many small ones
1249
+ const stream = new StreamText(filter, decoder, options);
1250
+ for await (const chunk of isReadableStream(input) ? streamChunks(input, signal) : input) {
1251
+ throwIfAborted(signal);
1252
+ const text = stream.push(chunk);
1253
+ if (text.length > 0) {
1254
+ yield text;
1255
+ }
1256
+ options.onProgress?.(stream.done);
1257
+ }
1258
+ const tail = stream.end();
1259
+ if (tail.length > 0) {
1260
+ yield tail;
1261
+ }
1262
+ filter.end();
1263
+ options.onProgress?.(stream.done, stream.done);
1264
+ }
1265
+
1266
+ /**
1267
+ * The end of the slice of a string input that starts at start: DECODE_SLICE code units, one more
1268
+ * when the slice would end between the two halves of a surrogate pair.
1269
+ * @param input - the string
1270
+ * @param start - where the slice starts
1271
+ * @returns where it ends
1272
+ */
1273
+ function stringSliceEnd(input: string, start: number): number {
1274
+ const end = Math.min(start + DECODE_SLICE, input.length);
1275
+ const highSurrogate = (input.charCodeAt(end - 1) & 0xfc00) === 0xd800; // NOSONAR(S7758): reads UTF-16 code units on purpose
1276
+ return end < input.length && highSurrogate ? end + 1 : end;
1277
+ }
1278
+
1279
+ /**
1280
+ * textChunks() of a string input: slices of it, through the text filter.
1281
+ * @param input - the string
1282
+ * @param filter - the text checks
1283
+ * @param options - cancellation and progress
1284
+ * @yields the filtered slices
1285
+ * @returns nothing
1286
+ */
1287
+ function* stringTextChunks(
1288
+ input: string,
1289
+ filter: TextFilter,
1290
+ options: ReadOptions,
1291
+ ): Generator<string, void, undefined> {
1292
+ const signal = options.signal ?? null;
1293
+ const onProgress = options.onProgress ?? null;
1294
+ const total = onProgress === null ? 0 : utf8Length(input);
1298
1295
  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) {
1296
+ let start = 0;
1297
+ while (start < input.length) {
1310
1298
  throwIfAborted(signal);
1311
- let text: string;
1312
- const bytes = typeof chunk === "string" ? null : asBytes(chunk);
1299
+ const end = stringSliceEnd(input, start);
1300
+ const piece = start === 0 && end === input.length ? input : input.slice(start, end);
1301
+ start = end;
1302
+ const text = filter.push(piece);
1303
+ if (text.length > 0) {
1304
+ yield text;
1305
+ }
1306
+ if (onProgress !== null) {
1307
+ done += utf8Length(piece);
1308
+ onProgress(done, total);
1309
+ }
1310
+ }
1311
+ if (input.length === 0) {
1312
+ onProgress?.(0, 0);
1313
+ }
1314
+ filter.end();
1315
+ }
1316
+
1317
+ /**
1318
+ * textChunks() of a Uint8Array input: slices of it, decoded, through the text filter.
1319
+ * @param input - the bytes
1320
+ * @param filter - the text checks
1321
+ * @param decoder - the byte decoder
1322
+ * @param options - cancellation, progress and the declaration size
1323
+ * @yields the decoded, filtered slices
1324
+ * @returns nothing
1325
+ */
1326
+ function* byteTextChunks(
1327
+ input: Uint8Array,
1328
+ filter: TextFilter,
1329
+ decoder: ByteDecoder,
1330
+ options: ReadOptions,
1331
+ ): Generator<string, void, undefined> {
1332
+ const signal = options.signal ?? null;
1333
+ const length = input.byteLength;
1334
+ decoder.start(input.subarray(0, headByteCount(options)));
1335
+ for (let offset = 0; offset < length; offset += DECODE_SLICE) {
1336
+ throwIfAborted(signal);
1337
+ if (input.byteLength !== length) {
1338
+ throw new GraphFormatError(
1339
+ "E_UNSUPPORTED",
1340
+ "the input's buffer was detached (transferred) or resized during the import",
1341
+ { reason: "detached" },
1342
+ );
1343
+ }
1344
+ const slice = input.subarray(offset, Math.min(offset + DECODE_SLICE, length));
1345
+ const text = filter.push(decoder.decode(slice, offset + DECODE_SLICE < length));
1346
+ if (text.length > 0) {
1347
+ yield text;
1348
+ }
1349
+ options.onProgress?.(Math.min(offset + DECODE_SLICE, length), length);
1350
+ }
1351
+ if (length === 0) {
1352
+ options.onProgress?.(0, 0);
1353
+ }
1354
+ filter.end();
1355
+ }
1356
+
1357
+ /**
1358
+ * How many leading bytes the encoding is chosen from: the format's declaration size, at least a BOM.
1359
+ * @param options - the declaration size
1360
+ * @returns the byte count
1361
+ */
1362
+ function headByteCount(options: ReadOptions): number {
1363
+ return Math.max(options.declarationBytes ?? HEAD_BYTES, BOM_BYTES);
1364
+ }
1365
+
1366
+ /**
1367
+ * The chunks of a stream input (a ReadableStream or an async iterable of strings and bytes) turned
1368
+ * into filtered text. The first bytes are held back until the BOM check (and the declaration
1369
+ * check, when the format has one) can see enough of them, or the input ended, or text arrived, so
1370
+ * a BOM or a declaration split across small chunks is still seen.
1371
+ */
1372
+ class StreamText {
1373
+ /** Bytes read so far (text counted as UTF-8 when progress is reported), for progress. */
1374
+ done = 0;
1375
+
1376
+ private head: Uint8Array[] = [];
1377
+
1378
+ private headLength = 0;
1379
+
1380
+ private readonly headBytes: number;
1381
+
1382
+ /** How many bytes to hold back before choosing the encoding. */
1383
+ private readonly needed: number;
1384
+
1385
+ /** Whether progress is reported, so text chunks are measured. */
1386
+ private readonly counting: boolean;
1387
+
1388
+ /**
1389
+ * Create the reader of one stream input.
1390
+ * @param filter - the text checks
1391
+ * @param decoder - the byte decoder
1392
+ * @param options - progress, the declaration reader and its size
1393
+ */
1394
+ constructor(
1395
+ private readonly filter: TextFilter,
1396
+ private readonly decoder: ByteDecoder,
1397
+ options: ReadOptions,
1398
+ ) {
1399
+ this.headBytes = headByteCount(options);
1400
+ this.needed = options.declaredEncoding === undefined ? BOM_BYTES : this.headBytes;
1401
+ this.counting = (options.onProgress ?? null) !== null;
1402
+ }
1403
+
1404
+ /**
1405
+ * Read the next chunk.
1406
+ * @param chunk - a string or a Uint8Array (anything else is E_UNSUPPORTED)
1407
+ * @returns its filtered text; empty while bytes are held back
1408
+ */
1409
+ push(chunk: unknown): string {
1313
1410
  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 {
1411
+ this.done += this.counting ? utf8Length(chunk) : 0;
1412
+ return this.filter.push(this.beforeText() + chunk);
1413
+ }
1414
+ const bytes = asBytes(chunk);
1415
+ if (bytes === null) {
1337
1416
  throw new GraphFormatError("E_UNSUPPORTED", "an input chunk must be a string or a Uint8Array", {
1338
1417
  reason: "chunk type",
1339
1418
  found: typeof chunk,
1340
1419
  });
1341
1420
  }
1342
- if (text.length > 0) {
1343
- yield text;
1421
+ this.done += bytes.byteLength;
1422
+ if (this.decoder.started) {
1423
+ return this.filter.push(this.decoder.decode(bytes, true));
1344
1424
  }
1345
- onProgress?.(done);
1425
+ this.head.push(bytes);
1426
+ this.headLength += bytes.byteLength;
1427
+ return this.headLength >= this.needed ? this.filter.push(this.flushHead(true)) : "";
1346
1428
  }
1347
- let tail = "";
1348
- if (headLength > 0) {
1349
- tail = flushHead(false);
1350
- } else if (decoder.started) {
1351
- tail = decoder.decode(new Uint8Array(0), false);
1429
+
1430
+ /**
1431
+ * The input ended: the filtered text of whatever is still held or pending.
1432
+ * @returns the text
1433
+ */
1434
+ end(): string {
1435
+ let tail = "";
1436
+ if (this.headLength > 0) {
1437
+ tail = this.flushHead(false);
1438
+ } else if (this.decoder.started) {
1439
+ tail = this.decoder.decode(new Uint8Array(0), false);
1440
+ }
1441
+ return this.filter.push(tail);
1352
1442
  }
1353
- tail = filter.push(tail);
1354
- if (tail.length > 0) {
1355
- yield tail;
1443
+
1444
+ /**
1445
+ * Start the decoder on the held bytes and decode them.
1446
+ * @param stream - whether more bytes follow
1447
+ * @returns their text
1448
+ */
1449
+ private flushHead(stream: boolean): string {
1450
+ const bytes = concatBytes(this.head);
1451
+ this.head = [];
1452
+ this.headLength = 0;
1453
+ this.decoder.start(bytes.subarray(0, this.headBytes));
1454
+ return this.decoder.decode(bytes, stream);
1455
+ }
1456
+
1457
+ /**
1458
+ * A text chunk follows: finish any byte sequence still pending in the decoder before switching
1459
+ * to text. The BOM and declaration checks apply to the start of the input only, so bytes after
1460
+ * text are never taken for a BOM.
1461
+ * @returns the text of the pending bytes
1462
+ */
1463
+ private beforeText(): string {
1464
+ if (this.headLength > 0) {
1465
+ return this.decoder.flushBeforeText(() => this.flushHead(false));
1466
+ }
1467
+ if (this.decoder.started) {
1468
+ return this.decoder.flushBeforeText(() => this.decoder.decode(new Uint8Array(0), false));
1469
+ }
1470
+ this.decoder.start(new Uint8Array(0));
1471
+ return "";
1356
1472
  }
1357
- filter.end();
1358
- onProgress?.(done, done);
1359
1473
  }
1360
1474
 
1361
1475
  /**
@@ -1493,10 +1493,11 @@ async function* blockElements(
1493
1493
  * @category Plugin helpers
1494
1494
  */
1495
1495
  export function reportTooDeep(report: ImportReportBuilder, event: Extract<AspectEvent, { kind: "deep" }>): void {
1496
+ const what = event.aspect === null ? "a member of the document" : `an element of "${event.aspect}"`;
1496
1497
  report.error(
1497
1498
  "parse-error",
1498
1499
  BAD_ASPECT_BLOCK_CODE,
1499
- `${event.aspect === null ? "a member of the document" : `an element of "${event.aspect}"`} is nested ${event.depth} levels deep, more than the ${MAX_ELEMENT_DEPTH} a graph keeps; skipped`,
1500
+ `${what} is nested ${event.depth} levels deep, more than the ${MAX_ELEMENT_DEPTH} a graph keeps; skipped`,
1500
1501
  { line: event.line, element: event.aspect },
1501
1502
  );
1502
1503
  }
@@ -1893,7 +1894,8 @@ export class CxStructure {
1893
1894
  }
1894
1895
  const error = typeof value.error === "string" ? value.error : "";
1895
1896
  if (value.success === false) {
1896
- const message = `the producer marked the document as failed${error.length > 0 ? `: ${error}` : ""}`;
1897
+ const detail = error.length > 0 ? `: ${error}` : "";
1898
+ const message = `the producer marked the document as failed${detail}`;
1897
1899
  this.report.error("validation-error", STATUS_FAILED_CODE, message, { line, element: "status" });
1898
1900
  throw this.report.abort(message, { code: STATUS_FAILED_CODE });
1899
1901
  }
@@ -202,21 +202,7 @@ export class TextCellWriter {
202
202
  write(row: number, text: string): void {
203
203
  const kind = inferTextDtype(text);
204
204
  const value = parseTextCell(text);
205
- if (
206
- typeof value === "number" &&
207
- !Number.isSafeInteger(value) &&
208
- I32_TEXT.test(text) &&
209
- BigInt(text) !== BigInt(value)
210
- ) {
211
- // an integer beyond 2^53 is stored as the nearest double; say so once per column
212
- this.report.warnOnce(
213
- "precision",
214
- PRECISION_CODE,
215
- `${this.domain} column "${this.name}": integers beyond 2^53 are stored as the nearest double (first: ${text})`,
216
- { element: this.name },
217
- `${PRECISION_CODE}:${this.domain}:${this.name}`,
218
- );
219
- }
205
+ this.warnIfImprecise(text, value);
220
206
  const wasString = this.textDtype === "string";
221
207
  const textDtype =
222
208
  this.textDtype === null || textDtypeRank(kind) > textDtypeRank(this.textDtype) ? kind : this.textDtype;
@@ -250,6 +236,29 @@ export class TextCellWriter {
250
236
  }
251
237
  }
252
238
 
239
+ /**
240
+ * Warn, once per column, when the cell is an integer beyond 2^53 that is stored as the nearest
241
+ * double.
242
+ * @param text - the cell text
243
+ * @param value - the parsed value
244
+ */
245
+ private warnIfImprecise(text: string, value: unknown): void {
246
+ if (
247
+ typeof value === "number" &&
248
+ !Number.isSafeInteger(value) &&
249
+ I32_TEXT.test(text) &&
250
+ BigInt(text) !== BigInt(value)
251
+ ) {
252
+ this.report.warnOnce(
253
+ "precision",
254
+ PRECISION_CODE,
255
+ `${this.domain} column "${this.name}": integers beyond 2^53 are stored as the nearest double (first: ${text})`,
256
+ { element: this.name },
257
+ `${PRECISION_CODE}:${this.domain}:${this.name}`,
258
+ );
259
+ }
260
+ }
261
+
253
262
  /**
254
263
  * Write a value (the parsed scalar, or a text into a widened column).
255
264
  * @param row - the row
package/src/common/xml.ts CHANGED
@@ -416,7 +416,8 @@ export function decodeEntities(raw: string, line: number, repairs?: XmlRepairs):
416
416
  const digits = name.slice(hex ? 2 : 1);
417
417
  // XML 1.0 puts no limit on leading zeros (&#x0000000041; is "A")
418
418
  const ok = hex ? /^0*[\da-fA-F]{1,6}$/.test(digits) : /^0*\d{1,7}$/.test(digits);
419
- const cp = ok ? Number.parseInt(digits, hex ? 16 : 10) : -1;
419
+ const radix = hex ? 16 : 10;
420
+ const cp = ok ? Number.parseInt(digits, radix) : -1;
420
421
  const low = cp >= 0xd800 && cp <= 0xdbff ? lowSurrogateAfter(raw, semi + 1, repairs) : null;
421
422
  if (low !== null) {
422
423
  repairs?.surrogatePair?.(lineAt(raw, amp, line));
package/src/common/zip.ts CHANGED
@@ -134,6 +134,52 @@ export function startsLikeZip(bytes: Uint8Array): boolean {
134
134
  return bytes.byteLength >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 3 && bytes[3] === 4;
135
135
  }
136
136
 
137
+ /**
138
+ * Read the end of central directory record, following the zip64 locator when there is one.
139
+ * @param view - the whole archive
140
+ * @param eocd - the offset of the end record
141
+ * @returns the disk numbers, the entry count and where the central directory is; ZipError
142
+ * "corrupt" when the end record needs a zip64 record the archive does not have
143
+ */
144
+ function readEndRecord(
145
+ view: DataView,
146
+ eocd: number,
147
+ ): {
148
+ disk: number;
149
+ directoryDisk: number;
150
+ count: number;
151
+ directorySize: number;
152
+ directoryOffset: number;
153
+ directoryEnd: number;
154
+ } {
155
+ const locator = eocd - ZIP64_LOCATOR_SIZE;
156
+ if (locator >= 0 && view.getUint32(locator, true) === ZIP64_LOCATOR_SIGNATURE) {
157
+ const record = zip64EndRecord(view, locator);
158
+ return {
159
+ disk: view.getUint32(record + 16, true),
160
+ directoryDisk: view.getUint32(record + 20, true),
161
+ count: readU64(view, record + 32),
162
+ directorySize: readU64(view, record + 40),
163
+ directoryOffset: readU64(view, record + 48),
164
+ directoryEnd: record,
165
+ };
166
+ }
167
+ const count = view.getUint16(eocd + 10, true);
168
+ const directorySize = view.getUint32(eocd + 12, true);
169
+ const directoryOffset = view.getUint32(eocd + 16, true);
170
+ if (count === U16_MAX || directorySize === U32_MAX || directoryOffset === U32_MAX) {
171
+ throw new ZipError("corrupt", "the end record needs a zip64 record the archive does not have");
172
+ }
173
+ return {
174
+ disk: view.getUint16(eocd + 4, true),
175
+ directoryDisk: view.getUint16(eocd + 6, true),
176
+ count,
177
+ directorySize,
178
+ directoryOffset,
179
+ directoryEnd: eocd,
180
+ };
181
+ }
182
+
137
183
  /**
138
184
  * Read the central directory.
139
185
  * @param bytes - the whole archive
@@ -150,24 +196,7 @@ export function readZipDirectory(bytes: Uint8Array): ZipEntry[] {
150
196
  ? new ZipError("corrupt", "the archive has no end of central directory record (truncated?)")
151
197
  : new ZipError("not-zip", "the input is not a zip archive");
152
198
  }
153
- let disk = view.getUint16(eocd + 4, true);
154
- let directoryDisk = view.getUint16(eocd + 6, true);
155
- let count = view.getUint16(eocd + 10, true);
156
- let directorySize = view.getUint32(eocd + 12, true);
157
- let directoryOffset = view.getUint32(eocd + 16, true);
158
- let directoryEnd = eocd;
159
- const locator = eocd - ZIP64_LOCATOR_SIZE;
160
- if (locator >= 0 && view.getUint32(locator, true) === ZIP64_LOCATOR_SIGNATURE) {
161
- const record = zip64EndRecord(view, locator);
162
- disk = view.getUint32(record + 16, true);
163
- directoryDisk = view.getUint32(record + 20, true);
164
- count = readU64(view, record + 32);
165
- directorySize = readU64(view, record + 40);
166
- directoryOffset = readU64(view, record + 48);
167
- directoryEnd = record;
168
- } else if (count === U16_MAX || directorySize === U32_MAX || directoryOffset === U32_MAX) {
169
- throw new ZipError("corrupt", "the end record needs a zip64 record the archive does not have");
170
- }
199
+ const { disk, directoryDisk, count, directorySize, directoryOffset, directoryEnd } = readEndRecord(view, eocd);
171
200
  if (disk !== 0 || directoryDisk !== 0) {
172
201
  throw new ZipError("unsupported", "a split (multi-disk) archive cannot be read");
173
202
  }