@graphty/graph-io 0.3.40 → 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.
- package/dist/chunks/{choose-CS3fvGT1.js → choose-CwFQamH9.js} +2 -2
- package/dist/chunks/{choose-CS3fvGT1.js.map → choose-CwFQamH9.js.map} +1 -1
- package/dist/chunks/{constants-DqKZT6cH.js → constants-Cf4Nn6tw.js} +2 -2
- package/dist/chunks/{constants-DqKZT6cH.js.map → constants-Cf4Nn6tw.js.map} +1 -1
- package/dist/chunks/{escape-BJRLd3vJ.js → escape-DBb8zuNE.js} +2 -2
- package/dist/chunks/{escape-BJRLd3vJ.js.map → escape-DBb8zuNE.js.map} +1 -1
- package/dist/chunks/{export-B-aVCwyG.js → export-DD3Lvw-F.js} +173 -116
- package/dist/chunks/export-DD3Lvw-F.js.map +1 -0
- package/dist/chunks/{exporter-Bcp-l4Bo.js → exporter-BqtmlxNr.js} +545 -459
- package/dist/chunks/exporter-BqtmlxNr.js.map +1 -0
- package/dist/chunks/{exporter-6ocL_9x_.js → exporter-CS35R6Fg.js} +4 -4
- package/dist/chunks/{exporter-6ocL_9x_.js.map → exporter-CS35R6Fg.js.map} +1 -1
- package/dist/chunks/{exporter-BVXnV9-9.js → exporter-D8r1N2dU.js} +28 -16
- package/dist/chunks/exporter-D8r1N2dU.js.map +1 -0
- package/dist/chunks/{exporter-SFYkDzD0.js → exporter-NMplSnYn.js} +23 -20
- package/dist/chunks/{exporter-SFYkDzD0.js.map → exporter-NMplSnYn.js.map} +1 -1
- package/dist/chunks/{importer-CPqnIY8m.js → importer-BKC7AmCA.js} +42 -29
- package/dist/chunks/importer-BKC7AmCA.js.map +1 -0
- package/dist/chunks/{importer-D_srHyj6.js → importer-BdWHmlmG.js} +4 -4
- package/dist/chunks/{importer-D_srHyj6.js.map → importer-BdWHmlmG.js.map} +1 -1
- package/dist/chunks/{importer-AjwOJrn2.js → importer-BfbzWk6k.js} +202 -181
- package/dist/chunks/importer-BfbzWk6k.js.map +1 -0
- package/dist/chunks/{importer-BpuS3J8C.js → importer-BmaZcskq.js} +73 -60
- package/dist/chunks/importer-BmaZcskq.js.map +1 -0
- package/dist/chunks/{importer-DEp_fYjf.js → importer-DviEsHy4.js} +92 -64
- package/dist/chunks/importer-DviEsHy4.js.map +1 -0
- package/dist/chunks/{importer-DcKB97kW.js → importer-S-abxJHn.js} +5 -5
- package/dist/chunks/{importer-DcKB97kW.js.map → importer-S-abxJHn.js.map} +1 -1
- package/dist/chunks/{importer-BJlGAOht.js → importer-r29cWnEf.js} +3 -3
- package/dist/chunks/{importer-BJlGAOht.js.map → importer-r29cWnEf.js.map} +1 -1
- package/dist/chunks/{json-elements-DpO6AXIc.js → json-elements-DdF-etPT.js} +3 -3
- package/dist/chunks/{json-elements-DpO6AXIc.js.map → json-elements-DdF-etPT.js.map} +1 -1
- package/dist/chunks/{ontology-export-50L9DOV4.js → ontology-export-BC0zyPhj.js} +2 -2
- package/dist/chunks/{ontology-export-50L9DOV4.js.map → ontology-export-BC0zyPhj.js.map} +1 -1
- package/dist/chunks/{records-r3SFHz11.js → records-Uc3LkABx.js} +15 -12
- package/dist/chunks/records-Uc3LkABx.js.map +1 -0
- package/dist/chunks/{text-Bjfa1xYL.js → text-Bh9SaAob.js} +20 -11
- package/dist/chunks/text-Bh9SaAob.js.map +1 -0
- package/dist/csv.js +67 -49
- package/dist/csv.js.map +1 -1
- package/dist/cx.js +1 -1
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +195 -97
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +170 -149
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +26 -26
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/neo4j.js +103 -65
- package/dist/neo4j.js.map +1 -1
- package/dist/obo.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/attributes.d.ts.map +1 -1
- package/dist/src/common/attributes.js +34 -9
- package/dist/src/common/attributes.js.map +1 -1
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +180 -102
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/text.d.ts +7 -0
- package/dist/src/common/text.d.ts.map +1 -1
- package/dist/src/common/text.js +15 -7
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/zip.d.ts.map +1 -1
- package/dist/src/common/zip.js +36 -19
- package/dist/src/common/zip.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +77 -25
- package/dist/src/formats/csv/exporter.js.map +1 -1
- package/dist/src/formats/csv/importer.d.ts.map +1 -1
- package/dist/src/formats/csv/importer.js +16 -6
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/records.d.ts.map +1 -1
- package/dist/src/formats/csv/records.js +18 -10
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/cys/tables.d.ts.map +1 -1
- package/dist/src/formats/cys/tables.js +11 -8
- package/dist/src/formats/cys/tables.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +65 -37
- package/dist/src/formats/gexf/exporter.js.map +1 -1
- package/dist/src/formats/gexf/importer.d.ts.map +1 -1
- package/dist/src/formats/gexf/importer.js +138 -50
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gml/exporter.d.ts.map +1 -1
- package/dist/src/formats/gml/exporter.js +51 -35
- package/dist/src/formats/gml/exporter.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +103 -58
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/syntax.d.ts.map +1 -1
- package/dist/src/formats/gml/syntax.js +17 -8
- package/dist/src/formats/gml/syntax.js.map +1 -1
- package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
- package/dist/src/formats/graphml/exporter.js +34 -24
- package/dist/src/formats/graphml/exporter.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +17 -13
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/graphml/tree.js +23 -14
- package/dist/src/formats/graphml/tree.js.map +1 -1
- package/dist/src/formats/graphml/yfiles.d.ts.map +1 -1
- package/dist/src/formats/graphml/yfiles.js +22 -10
- package/dist/src/formats/graphml/yfiles.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +38 -21
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +536 -343
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/json/obographs.js +29 -17
- package/dist/src/formats/json/obographs.js.map +1 -1
- package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
- package/dist/src/formats/neo4j/exporter.js +73 -43
- package/dist/src/formats/neo4j/exporter.js.map +1 -1
- package/dist/src/formats/neo4j/header.d.ts.map +1 -1
- package/dist/src/formats/neo4j/header.js +37 -16
- package/dist/src/formats/neo4j/header.js.map +1 -1
- package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
- package/dist/src/formats/neo4j/importer.js +21 -11
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/obo/exporter.d.ts.map +1 -1
- package/dist/src/formats/obo/exporter.js +18 -7
- package/dist/src/formats/obo/exporter.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +142 -73
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +26 -15
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
- package/dist/src/formats/pajek/syntax.js +85 -58
- package/dist/src/formats/pajek/syntax.js.map +1 -1
- package/dist/src/formats/xgmml/emit.d.ts +7 -0
- package/dist/src/formats/xgmml/emit.d.ts.map +1 -1
- package/dist/src/formats/xgmml/emit.js +79 -40
- package/dist/src/formats/xgmml/emit.js.map +1 -1
- package/dist/src/formats/xgmml/exporter.d.ts.map +1 -1
- package/dist/src/formats/xgmml/exporter.js +38 -12
- package/dist/src/formats/xgmml/exporter.js.map +1 -1
- package/dist/src/formats/xgmml/importer.d.ts.map +1 -1
- package/dist/src/formats/xgmml/importer.js +38 -18
- package/dist/src/formats/xgmml/importer.js.map +1 -1
- package/dist/src/sniff.js +109 -57
- package/dist/src/sniff.js.map +1 -1
- package/dist/xgmml.js +2 -2
- package/package.json +2 -2
- package/src/common/attributes.ts +59 -19
- package/src/common/input.ts +216 -102
- package/src/common/text.ts +24 -15
- package/src/common/zip.ts +47 -18
- package/src/formats/csv/exporter.ts +103 -39
- package/src/formats/csv/importer.ts +17 -6
- package/src/formats/csv/records.ts +19 -10
- package/src/formats/cys/tables.ts +11 -10
- package/src/formats/gexf/exporter.ts +67 -36
- package/src/formats/gexf/importer.ts +173 -63
- package/src/formats/gml/exporter.ts +78 -82
- package/src/formats/gml/importer.ts +119 -68
- package/src/formats/gml/syntax.ts +18 -8
- package/src/formats/graphml/exporter.ts +44 -24
- package/src/formats/graphml/importer.ts +18 -11
- package/src/formats/graphml/tree.ts +22 -12
- package/src/formats/graphml/yfiles.ts +23 -10
- package/src/formats/json/exporter.ts +40 -21
- package/src/formats/json/importer.ts +701 -385
- package/src/formats/json/obographs.ts +35 -15
- package/src/formats/neo4j/exporter.ts +79 -41
- package/src/formats/neo4j/header.ts +50 -16
- package/src/formats/neo4j/importer.ts +21 -10
- package/src/formats/obo/exporter.ts +34 -17
- package/src/formats/pajek/exporter.ts +180 -117
- package/src/formats/pajek/importer.ts +27 -15
- package/src/formats/pajek/syntax.ts +99 -58
- package/src/formats/xgmml/emit.ts +88 -40
- package/src/formats/xgmml/exporter.ts +40 -12
- package/src/formats/xgmml/importer.ts +45 -23
- package/src/sniff.ts +115 -59
- package/dist/chunks/export-B-aVCwyG.js.map +0 -1
- package/dist/chunks/exporter-BVXnV9-9.js.map +0 -1
- package/dist/chunks/exporter-Bcp-l4Bo.js.map +0 -1
- package/dist/chunks/importer-AjwOJrn2.js.map +0 -1
- package/dist/chunks/importer-BpuS3J8C.js.map +0 -1
- package/dist/chunks/importer-CPqnIY8m.js.map +0 -1
- package/dist/chunks/importer-DEp_fYjf.js.map +0 -1
- package/dist/chunks/records-r3SFHz11.js.map +0 -1
- package/dist/chunks/text-Bjfa1xYL.js.map +0 -1
package/src/common/input.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
1295
|
-
//
|
|
1296
|
-
|
|
1297
|
-
|
|
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
|
|
1300
|
-
|
|
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
|
-
|
|
1312
|
-
const
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
pending = decoder.flushBeforeText(() => flushHead(false));
|
|
1320
|
-
} else if (decoder.started) {
|
|
1321
|
-
pending = decoder.flushBeforeText(() => decoder.decode(new Uint8Array(0), false));
|
|
1322
|
-
} else {
|
|
1323
|
-
decoder.start(new Uint8Array(0));
|
|
1324
|
-
}
|
|
1325
|
-
text = filter.push(pending + chunk);
|
|
1326
|
-
done += onProgress === null ? 0 : utf8Length(chunk);
|
|
1327
|
-
} else if (bytes !== null) {
|
|
1328
|
-
done += bytes.byteLength;
|
|
1329
|
-
if (decoder.started) {
|
|
1330
|
-
text = filter.push(decoder.decode(bytes, true));
|
|
1331
|
-
} else {
|
|
1332
|
-
head.push(bytes);
|
|
1333
|
-
headLength += bytes.byteLength;
|
|
1334
|
-
text = headLength >= needed ? filter.push(flushHead(true)) : "";
|
|
1335
|
-
}
|
|
1336
|
-
} else {
|
|
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
|
-
|
|
1343
|
-
|
|
1421
|
+
this.done += bytes.byteLength;
|
|
1422
|
+
if (this.decoder.started) {
|
|
1423
|
+
return this.filter.push(this.decoder.decode(bytes, true));
|
|
1344
1424
|
}
|
|
1345
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
/**
|
package/src/common/text.ts
CHANGED
|
@@ -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
|
-
|
|
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/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
|
-
|
|
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
|
}
|
|
@@ -407,6 +407,25 @@ function nodeTableNote(names: readonly string[], table: string, notes: LossNote[
|
|
|
407
407
|
}
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The number of node ids whose text an earlier id already has (a number and a string).
|
|
412
|
+
* @param ids - the snapshot's ids
|
|
413
|
+
* @returns the collisions
|
|
414
|
+
*/
|
|
415
|
+
function idTextCollisions(ids: GraphSnapshot["ids"]): number {
|
|
416
|
+
const seen = new Set<string>();
|
|
417
|
+
let collisions = 0;
|
|
418
|
+
for (let i = 0; i < ids.size; i++) {
|
|
419
|
+
const text = String(ids.idOf(i));
|
|
420
|
+
if (seen.has(text)) {
|
|
421
|
+
collisions++;
|
|
422
|
+
} else {
|
|
423
|
+
seen.add(text);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return collisions;
|
|
427
|
+
}
|
|
428
|
+
|
|
410
429
|
/**
|
|
411
430
|
* The id notes: text collisions are fatal (export() throws E_INVALID_ID); type changes under the
|
|
412
431
|
* canonical re-read are reported.
|
|
@@ -421,16 +440,7 @@ function idNotes(
|
|
|
421
440
|
): void {
|
|
422
441
|
const { ids } = snapshot;
|
|
423
442
|
if (ids.kind === "mixed") {
|
|
424
|
-
const
|
|
425
|
-
let collisions = 0;
|
|
426
|
-
for (let i = 0; i < ids.size; i++) {
|
|
427
|
-
const text = String(ids.idOf(i));
|
|
428
|
-
if (seen.has(text)) {
|
|
429
|
-
collisions++;
|
|
430
|
-
} else {
|
|
431
|
-
seen.add(text);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
443
|
+
const collisions = idTextCollisions(ids);
|
|
434
444
|
if (collisions > 0) {
|
|
435
445
|
note(
|
|
436
446
|
CSV_LOSS.ID_TEXT_COLLISION,
|
|
@@ -699,40 +709,81 @@ function attributeColumns(
|
|
|
699
709
|
if (role !== null && SKIPPED_ROLES.has(role)) {
|
|
700
710
|
continue;
|
|
701
711
|
}
|
|
702
|
-
const low = lower(name);
|
|
703
712
|
const set = column.length - column.nullCount;
|
|
704
|
-
|
|
705
|
-
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
706
|
-
note?.(
|
|
707
|
-
CSV_LOSS.RESERVED_NAME,
|
|
708
|
-
`${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`,
|
|
709
|
-
name,
|
|
710
|
-
set,
|
|
711
|
-
);
|
|
713
|
+
if (isReservedName(name, domain, reserved, idColumn, set, note)) {
|
|
712
714
|
continue;
|
|
713
715
|
}
|
|
714
|
-
if (findColumn([name], LABEL_NAMES) >= 0) {
|
|
715
|
-
|
|
716
|
-
note?.(
|
|
717
|
-
CSV_LOSS.RESERVED_NAME,
|
|
718
|
-
`${domain} column "${name}" is not written: the name is reserved for the label column`,
|
|
719
|
-
name,
|
|
720
|
-
set,
|
|
721
|
-
);
|
|
722
|
-
continue;
|
|
723
|
-
}
|
|
724
|
-
note?.(
|
|
725
|
-
CSV_LOSS.ROLE_ASSUMED,
|
|
726
|
-
`${domain} column "${name}" reads back with the label role (string)`,
|
|
727
|
-
name,
|
|
728
|
-
set,
|
|
729
|
-
);
|
|
716
|
+
if (findColumn([name], LABEL_NAMES) >= 0 && !labelNameWritten(name, domain, labelColumn, set, note)) {
|
|
717
|
+
continue;
|
|
730
718
|
}
|
|
731
719
|
out.push({ column, header: name });
|
|
732
720
|
}
|
|
733
721
|
return out;
|
|
734
722
|
}
|
|
735
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Whether an attribute column's name is reserved for a structural column (a dialect header, an
|
|
726
|
+
* edge id name, the id column's name), with its note.
|
|
727
|
+
* @param name - the column name
|
|
728
|
+
* @param domain - node or edge, for messages
|
|
729
|
+
* @param reserved - lower-case header names the dialect writes for structure
|
|
730
|
+
* @param idColumn - the table's id role column, or null
|
|
731
|
+
* @param set - the column's set cells
|
|
732
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
733
|
+
* @returns true when the column is not written
|
|
734
|
+
*/
|
|
735
|
+
function isReservedName(
|
|
736
|
+
name: string,
|
|
737
|
+
domain: "node" | "edge",
|
|
738
|
+
reserved: ReadonlySet<string>,
|
|
739
|
+
idColumn: Column | null,
|
|
740
|
+
set: number,
|
|
741
|
+
note: ((code: string, message: string, column?: string | null, count?: number | null) => void) | null,
|
|
742
|
+
): boolean {
|
|
743
|
+
const low = lower(name);
|
|
744
|
+
const isIdName = domain === "edge" ? findColumn([name], EDGE_ID_NAMES) >= 0 : false;
|
|
745
|
+
if (reserved.has(low) || isIdName || (idColumn !== null && lower(idColumn.meta.name) === low)) {
|
|
746
|
+
note?.(
|
|
747
|
+
CSV_LOSS.RESERVED_NAME,
|
|
748
|
+
`${domain} column "${name}" is not written: the name is reserved for the ${isIdName ? "edge id" : low} column`,
|
|
749
|
+
name,
|
|
750
|
+
set,
|
|
751
|
+
);
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Whether an attribute column with a label header name is written, with its note: not when the
|
|
759
|
+
* table has a label column (the name is reserved for it), else it reads back with the label role.
|
|
760
|
+
* @param name - the column name
|
|
761
|
+
* @param domain - node or edge, for messages
|
|
762
|
+
* @param labelColumn - the table's label role column, or null
|
|
763
|
+
* @param set - the column's set cells
|
|
764
|
+
* @param note - the recorder, or null when this table's notes are not wanted
|
|
765
|
+
* @returns true when the column is written
|
|
766
|
+
*/
|
|
767
|
+
function labelNameWritten(
|
|
768
|
+
name: string,
|
|
769
|
+
domain: "node" | "edge",
|
|
770
|
+
labelColumn: Column | null,
|
|
771
|
+
set: number,
|
|
772
|
+
note: ((code: string, message: string, column?: string | null, count?: number | null) => void) | null,
|
|
773
|
+
): boolean {
|
|
774
|
+
if (labelColumn !== null) {
|
|
775
|
+
note?.(
|
|
776
|
+
CSV_LOSS.RESERVED_NAME,
|
|
777
|
+
`${domain} column "${name}" is not written: the name is reserved for the label column`,
|
|
778
|
+
name,
|
|
779
|
+
set,
|
|
780
|
+
);
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
note?.(CSV_LOSS.ROLE_ASSUMED, `${domain} column "${name}" reads back with the label role (string)`, name, set);
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
|
|
736
787
|
/**
|
|
737
788
|
* Notes about a role column (id or label): a name the importer does not map to the role, and a
|
|
738
789
|
* dtype other than string / dict (read back as string).
|
|
@@ -912,6 +963,22 @@ function neighbourCell(id: string, weight: string | null): string {
|
|
|
912
963
|
return id.includes(":") ? `${id}:` : id;
|
|
913
964
|
}
|
|
914
965
|
|
|
966
|
+
/**
|
|
967
|
+
* The end of the run of consecutive edge rows that share the source of row `r`.
|
|
968
|
+
* @param src - the edge sources by edge index
|
|
969
|
+
* @param edgeRows - the edges in write order
|
|
970
|
+
* @param r - the run's first row
|
|
971
|
+
* @returns the index after the run's last row
|
|
972
|
+
*/
|
|
973
|
+
function sourceRunEnd(src: ArrayLike<number>, edgeRows: ArrayLike<number>, r: number): number {
|
|
974
|
+
const source = src[edgeRows[r]];
|
|
975
|
+
let end = r;
|
|
976
|
+
while (end < edgeRows.length && src[edgeRows[end]] === source) {
|
|
977
|
+
end++;
|
|
978
|
+
}
|
|
979
|
+
return end;
|
|
980
|
+
}
|
|
981
|
+
|
|
915
982
|
/**
|
|
916
983
|
* The rows of the adjacency table (design section 8.5: the order survives the importer's first-
|
|
917
984
|
* appearance numbering). Consecutive edges with one source share a row, so the edge order is kept;
|
|
@@ -953,10 +1020,7 @@ function* adjacencyLines(snapshot: GraphSnapshot, plan: Plan): Generator<string,
|
|
|
953
1020
|
let r = 0;
|
|
954
1021
|
while (r < edgeRows.length) {
|
|
955
1022
|
const source = list.src[edgeRows[r]];
|
|
956
|
-
|
|
957
|
-
while (end < edgeRows.length && list.src[edgeRows[end]] === source) {
|
|
958
|
-
end++;
|
|
959
|
-
}
|
|
1023
|
+
const end = sourceRunEnd(list.src, edgeRows, r);
|
|
960
1024
|
// the nodes this row introduces, in the order the importer meets them; in index order from
|
|
961
1025
|
// `next` they need nothing more
|
|
962
1026
|
const fresh: number[] = [];
|