@graphty/graph-io 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -7
- package/dist/chunks/{escape-DyI8JofU.js → escape-CKied3Ri.js} +16 -10
- package/dist/chunks/escape-CKied3Ri.js.map +1 -0
- package/dist/chunks/{importer-DbnGYr3_.js → importer-C6QcIRIb.js} +129 -39
- package/dist/chunks/importer-C6QcIRIb.js.map +1 -0
- package/dist/chunks/{importer-CpCpfbxr.js → importer-D5pAsweZ.js} +136 -31
- package/dist/chunks/importer-D5pAsweZ.js.map +1 -0
- package/dist/chunks/{importer-GozH8DkN.js → importer-DbMrAR_w.js} +10 -4
- package/dist/chunks/importer-DbMrAR_w.js.map +1 -0
- package/dist/chunks/{importer-CQnJuWJw.js → importer-DkzjTvHc.js} +82 -40
- package/dist/chunks/importer-DkzjTvHc.js.map +1 -0
- package/dist/chunks/{records-CGpxszm1.js → records-BKSMowhR.js} +3 -3
- package/dist/chunks/{records-CGpxszm1.js.map → records-BKSMowhR.js.map} +1 -1
- package/dist/chunks/{text-CajMdVFy.js → text-Dr0Ifpag.js} +2 -2
- package/dist/chunks/{text-CajMdVFy.js.map → text-Dr0Ifpag.js.map} +1 -1
- package/dist/chunks/{writer-DxSKC7TL.js → writer-BdMak4_J.js} +341 -112
- package/dist/chunks/writer-BdMak4_J.js.map +1 -0
- package/dist/csv.js +24 -6
- package/dist/csv.js.map +1 -1
- package/dist/dot.js +1 -1
- package/dist/gexf.js +42 -10
- package/dist/gexf.js.map +1 -1
- package/dist/gml.js +69 -25
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +186 -129
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +1 -1
- package/dist/json.js +1 -1
- package/dist/neo4j.js +10 -4
- package/dist/neo4j.js.map +1 -1
- package/dist/pajek.js +1 -1
- package/dist/src/common/codes.d.ts +6 -0
- package/dist/src/common/codes.d.ts.map +1 -1
- package/dist/src/common/codes.js +6 -0
- package/dist/src/common/codes.js.map +1 -1
- package/dist/src/common/escape.d.ts +5 -4
- package/dist/src/common/escape.d.ts.map +1 -1
- package/dist/src/common/escape.js +6 -5
- package/dist/src/common/escape.js.map +1 -1
- package/dist/src/common/format.d.ts +3 -3
- package/dist/src/common/format.js +5 -5
- package/dist/src/common/format.js.map +1 -1
- package/dist/src/common/input.d.ts +24 -6
- package/dist/src/common/input.d.ts.map +1 -1
- package/dist/src/common/input.js +259 -23
- package/dist/src/common/input.js.map +1 -1
- package/dist/src/common/options.d.ts +2 -0
- package/dist/src/common/options.d.ts.map +1 -1
- package/dist/src/common/options.js +17 -0
- package/dist/src/common/options.js.map +1 -1
- package/dist/src/common/text.js +4 -4
- package/dist/src/common/text.js.map +1 -1
- package/dist/src/common/weights.d.ts.map +1 -1
- package/dist/src/common/weights.js +8 -0
- package/dist/src/common/weights.js.map +1 -1
- package/dist/src/common/xml.d.ts +7 -0
- package/dist/src/common/xml.d.ts.map +1 -1
- package/dist/src/common/xml.js +12 -0
- package/dist/src/common/xml.js.map +1 -1
- package/dist/src/formats/csv/exporter.d.ts +5 -1
- package/dist/src/formats/csv/exporter.d.ts.map +1 -1
- package/dist/src/formats/csv/exporter.js +8 -1
- 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 +1 -0
- package/dist/src/formats/csv/importer.js.map +1 -1
- package/dist/src/formats/csv/index.d.ts +6 -0
- package/dist/src/formats/csv/index.d.ts.map +1 -1
- package/dist/src/formats/csv/index.js +7 -1
- package/dist/src/formats/csv/index.js.map +1 -1
- package/dist/src/formats/csv/records.js +1 -1
- package/dist/src/formats/csv/records.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts +1 -1
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +3 -3
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts +6 -0
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +151 -24
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
- package/dist/src/formats/gexf/exporter.js +32 -7
- 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 +2 -2
- package/dist/src/formats/gexf/importer.js.map +1 -1
- package/dist/src/formats/gexf/index.d.ts +6 -0
- package/dist/src/formats/gexf/index.d.ts.map +1 -1
- package/dist/src/formats/gexf/index.js +7 -1
- package/dist/src/formats/gexf/index.js.map +1 -1
- package/dist/src/formats/gml/importer.d.ts +2 -2
- package/dist/src/formats/gml/importer.d.ts.map +1 -1
- package/dist/src/formats/gml/importer.js +66 -25
- package/dist/src/formats/gml/importer.js.map +1 -1
- package/dist/src/formats/gml/index.d.ts +7 -1
- package/dist/src/formats/gml/index.d.ts.map +1 -1
- package/dist/src/formats/gml/index.js +8 -2
- package/dist/src/formats/gml/index.js.map +1 -1
- package/dist/src/formats/graphml/constants.d.ts +6 -0
- package/dist/src/formats/graphml/constants.d.ts.map +1 -1
- package/dist/src/formats/graphml/constants.js +7 -1
- package/dist/src/formats/graphml/constants.js.map +1 -1
- package/dist/src/formats/graphml/importer.d.ts.map +1 -1
- package/dist/src/formats/graphml/importer.js +2 -2
- package/dist/src/formats/graphml/importer.js.map +1 -1
- package/dist/src/formats/json/exporter.d.ts.map +1 -1
- package/dist/src/formats/json/exporter.js +14 -5
- package/dist/src/formats/json/exporter.js.map +1 -1
- package/dist/src/formats/json/importer.d.ts +8 -0
- package/dist/src/formats/json/importer.d.ts.map +1 -1
- package/dist/src/formats/json/importer.js +84 -37
- package/dist/src/formats/json/importer.js.map +1 -1
- package/dist/src/formats/neo4j/importer.js +1 -1
- package/dist/src/formats/neo4j/importer.js.map +1 -1
- package/dist/src/formats/neo4j/index.d.ts +6 -0
- package/dist/src/formats/neo4j/index.d.ts.map +1 -1
- package/dist/src/formats/neo4j/index.js +7 -1
- package/dist/src/formats/neo4j/index.js.map +1 -1
- package/dist/src/formats/pajek/exporter.d.ts +2 -0
- package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
- package/dist/src/formats/pajek/exporter.js +13 -2
- package/dist/src/formats/pajek/exporter.js.map +1 -1
- package/dist/src/formats/pajek/importer.d.ts +8 -2
- package/dist/src/formats/pajek/importer.d.ts.map +1 -1
- package/dist/src/formats/pajek/importer.js +122 -26
- package/dist/src/formats/pajek/importer.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/registry.d.ts +26 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +101 -35
- package/dist/src/registry.js.map +1 -1
- package/dist/src/sniff.d.ts +1 -1
- package/dist/src/sniff.d.ts.map +1 -1
- package/dist/src/sniff.js +6 -1
- package/dist/src/sniff.js.map +1 -1
- package/dist/src/types.d.ts +22 -3
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js.map +1 -1
- package/package.json +4 -3
- package/src/common/codes.ts +9 -0
- package/src/common/escape.ts +6 -5
- package/src/common/format.ts +5 -5
- package/src/common/input.ts +293 -22
- package/src/common/options.ts +24 -0
- package/src/common/text.ts +4 -4
- package/src/common/weights.ts +9 -0
- package/src/common/xml.ts +14 -0
- package/src/formats/csv/exporter.ts +12 -1
- package/src/formats/csv/importer.ts +1 -0
- package/src/formats/csv/index.ts +9 -0
- package/src/formats/csv/records.ts +1 -1
- package/src/formats/dot/exporter.ts +3 -3
- package/src/formats/dot/importer.ts +172 -32
- package/src/formats/gexf/exporter.ts +38 -7
- package/src/formats/gexf/importer.ts +12 -2
- package/src/formats/gexf/index.ts +9 -0
- package/src/formats/gml/importer.ts +75 -22
- package/src/formats/gml/index.ts +10 -1
- package/src/formats/graphml/constants.ts +9 -0
- package/src/formats/graphml/importer.ts +9 -2
- package/src/formats/json/exporter.ts +14 -5
- package/src/formats/json/importer.ts +104 -36
- package/src/formats/neo4j/importer.ts +1 -1
- package/src/formats/neo4j/index.ts +9 -0
- package/src/formats/pajek/exporter.ts +19 -2
- package/src/formats/pajek/importer.ts +145 -28
- package/src/index.ts +1 -0
- package/src/registry.ts +131 -40
- package/src/sniff.ts +6 -1
- package/src/types.ts +26 -3
- package/dist/chunks/escape-DyI8JofU.js.map +0 -1
- package/dist/chunks/importer-CQnJuWJw.js.map +0 -1
- package/dist/chunks/importer-CpCpfbxr.js.map +0 -1
- package/dist/chunks/importer-DbnGYr3_.js.map +0 -1
- package/dist/chunks/importer-GozH8DkN.js.map +0 -1
- package/dist/chunks/writer-DxSKC7TL.js.map +0 -1
- package/dist/tsconfig.build.tsbuildinfo +0 -1
|
@@ -26,7 +26,16 @@ import {
|
|
|
26
26
|
} from "@graphty/graph-format";
|
|
27
27
|
|
|
28
28
|
import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE } from "../../common/attributes.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
DUPLICATE_NODE_CODE,
|
|
31
|
+
ENCODING_FALLBACK_CODE,
|
|
32
|
+
INVALID_ENCODING_CODE,
|
|
33
|
+
INVALID_UTF8_CODE,
|
|
34
|
+
MULTIPLE_GRAPHS_CODE,
|
|
35
|
+
OPTION_IGNORED_CODE,
|
|
36
|
+
SYNTAX_CODE,
|
|
37
|
+
UNKNOWN_ENCODING_CODE,
|
|
38
|
+
} from "../../common/codes.js";
|
|
30
39
|
import { DirectionResolver, type EdgeKind } from "../../common/direction.js";
|
|
31
40
|
import { coerceIdText, ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
|
|
32
41
|
import { LineReader, throwIfAborted } from "../../common/input.js";
|
|
@@ -75,12 +84,18 @@ export interface PajekImportOptions {
|
|
|
75
84
|
export const PAJEK_ISSUE = Object.freeze({
|
|
76
85
|
/** The input holds invalid UTF-8 (fatal). */
|
|
77
86
|
INVALID_UTF8: INVALID_UTF8_CODE,
|
|
87
|
+
/** Invalid bytes in the encoding a BOM, a declaration or the encoding option chose (fatal). */
|
|
88
|
+
INVALID_ENCODING: INVALID_ENCODING_CODE,
|
|
89
|
+
/** Bytes that are not UTF-8 and declare no encoding were read as windows-1252. */
|
|
90
|
+
ENCODING_FALLBACK: ENCODING_FALLBACK_CODE,
|
|
91
|
+
/** A declared encoding the platform cannot decode was ignored. */
|
|
92
|
+
UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
|
|
78
93
|
/** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
|
|
79
94
|
NO_VERTICES: "E_PAJEK_NO_VERTICES",
|
|
80
95
|
/** Fatal: `*Vertices` without a vertex count, or one the sink cannot hold. */
|
|
81
96
|
VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
|
|
82
|
-
/**
|
|
83
|
-
|
|
97
|
+
/** A project file holds several networks; import() reads the first, importAll() reads every one. */
|
|
98
|
+
MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
|
|
84
99
|
/** A data line before the first section header. */
|
|
85
100
|
OUTSIDE_SECTION: "E_PAJEK_OUTSIDE_SECTION",
|
|
86
101
|
/** A section header the importer cannot parse. */
|
|
@@ -329,6 +344,9 @@ class PajekParser {
|
|
|
329
344
|
|
|
330
345
|
private readonly nodeParams = new Map<string, TextCellWriter>();
|
|
331
346
|
|
|
347
|
+
/** Set when a header line starts the next network of a project file (that line is not consumed). */
|
|
348
|
+
private ended = false;
|
|
349
|
+
|
|
332
350
|
private readonly edgeParams = new Map<string, TextCellWriter>();
|
|
333
351
|
|
|
334
352
|
/**
|
|
@@ -356,15 +374,16 @@ class PajekParser {
|
|
|
356
374
|
* Handle one line; every problem of the line is recorded as an issue and the line skipped.
|
|
357
375
|
* @param text - the line without its terminator
|
|
358
376
|
* @param line - the 1-based line number
|
|
377
|
+
* @returns true when the line is the header of the next network (not consumed: this network is complete)
|
|
359
378
|
*/
|
|
360
|
-
line(text: string, line: number):
|
|
379
|
+
line(text: string, line: number): boolean {
|
|
361
380
|
if (isCommentOrBlank(text)) {
|
|
362
|
-
return;
|
|
381
|
+
return false;
|
|
363
382
|
}
|
|
364
383
|
try {
|
|
365
384
|
if (isSectionLine(text)) {
|
|
366
385
|
this.header(text, line);
|
|
367
|
-
return;
|
|
386
|
+
return this.ended;
|
|
368
387
|
}
|
|
369
388
|
switch (this.section) {
|
|
370
389
|
case "none":
|
|
@@ -375,18 +394,18 @@ class PajekParser {
|
|
|
375
394
|
);
|
|
376
395
|
case "vertices":
|
|
377
396
|
this.vertexLine(text, line);
|
|
378
|
-
return;
|
|
397
|
+
return false;
|
|
379
398
|
case "lines":
|
|
380
399
|
this.edgeLine(text, line);
|
|
381
|
-
return;
|
|
400
|
+
return false;
|
|
382
401
|
case "list":
|
|
383
402
|
this.listLine(text, line);
|
|
384
|
-
return;
|
|
403
|
+
return false;
|
|
385
404
|
case "matrix":
|
|
386
405
|
this.matrixLine(text, line);
|
|
387
|
-
return;
|
|
406
|
+
return false;
|
|
388
407
|
case "skip":
|
|
389
|
-
return;
|
|
408
|
+
return false;
|
|
390
409
|
default: {
|
|
391
410
|
const name: string = this.section;
|
|
392
411
|
throw new Error(`unknown section state ${name}`);
|
|
@@ -395,9 +414,10 @@ class PajekParser {
|
|
|
395
414
|
} catch (err) {
|
|
396
415
|
if (err instanceof LineError) {
|
|
397
416
|
this.report.error(err.category, err.code, err.message, { line });
|
|
398
|
-
return;
|
|
417
|
+
return false;
|
|
399
418
|
}
|
|
400
419
|
this.report.recordError(err, { line });
|
|
420
|
+
return false;
|
|
401
421
|
}
|
|
402
422
|
}
|
|
403
423
|
|
|
@@ -460,7 +480,8 @@ class PajekParser {
|
|
|
460
480
|
switch (h.kind) {
|
|
461
481
|
case "network":
|
|
462
482
|
if (this.networkName !== null || this.vertexCount >= 0) {
|
|
463
|
-
this.
|
|
483
|
+
this.ended = true;
|
|
484
|
+
return;
|
|
464
485
|
}
|
|
465
486
|
this.networkName = h.name ?? "";
|
|
466
487
|
this.section = "none";
|
|
@@ -471,6 +492,11 @@ class PajekParser {
|
|
|
471
492
|
this.section = "skip";
|
|
472
493
|
return;
|
|
473
494
|
}
|
|
495
|
+
if (this.vertexCount >= 0) {
|
|
496
|
+
// a second `*Vertices` without a `*Network` header starts the next network
|
|
497
|
+
this.ended = true;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
474
500
|
this.verticesHeader(h, line);
|
|
475
501
|
return;
|
|
476
502
|
case "arcs":
|
|
@@ -507,9 +533,6 @@ class PajekParser {
|
|
|
507
533
|
* @param line - the line number
|
|
508
534
|
*/
|
|
509
535
|
private verticesHeader(h: SectionHeader, line: number): void {
|
|
510
|
-
if (this.vertexCount >= 0) {
|
|
511
|
-
this.refuseSecondNetwork(h.keyword, line);
|
|
512
|
-
}
|
|
513
536
|
if (h.count === null) {
|
|
514
537
|
this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, "*Vertices needs a vertex count", { line });
|
|
515
538
|
}
|
|
@@ -572,17 +595,6 @@ class PajekParser {
|
|
|
572
595
|
this.relation = h.relation === null ? null : (h.name ?? String(h.relation));
|
|
573
596
|
}
|
|
574
597
|
|
|
575
|
-
/**
|
|
576
|
-
* Refuse a second network: an issue, then ImportError with the report so far.
|
|
577
|
-
* @param keyword - the header keyword
|
|
578
|
-
* @param line - the line number
|
|
579
|
-
*/
|
|
580
|
-
private refuseSecondNetwork(keyword: string, line: number): never {
|
|
581
|
-
const message = `*${keyword} starts a second network; the importer reads one network per file`;
|
|
582
|
-
this.report.error("unsupported", PAJEK_ISSUE.MULTIPLE_NETWORKS, message, { line });
|
|
583
|
-
throw this.report.abort(message, { code: PAJEK_ISSUE.MULTIPLE_NETWORKS, line });
|
|
584
|
-
}
|
|
585
|
-
|
|
586
598
|
/**
|
|
587
599
|
* Checks that run when a section ends.
|
|
588
600
|
*/
|
|
@@ -1296,16 +1308,121 @@ export const pajekImporter: GraphImporter<PajekImportOptions> = Object.freeze({
|
|
|
1296
1308
|
reportUnusedOptions(options, report, USED_OPTIONS);
|
|
1297
1309
|
const parser = new PajekParser(sink, report, resolved, firstVertex);
|
|
1298
1310
|
const reader = new LineReader(input, report, resolved);
|
|
1311
|
+
// after the first network: only its successors' headers are counted
|
|
1312
|
+
let rest: NetworkCounter | null = null;
|
|
1313
|
+
let restLine = 0;
|
|
1299
1314
|
let sinceCheck = 0;
|
|
1300
1315
|
for await (const text of reader) {
|
|
1301
|
-
|
|
1316
|
+
if (rest !== null) {
|
|
1317
|
+
rest.line(text);
|
|
1318
|
+
} else if (parser.line(text, reader.line)) {
|
|
1319
|
+
rest = new NetworkCounter();
|
|
1320
|
+
rest.line(text);
|
|
1321
|
+
restLine = reader.line;
|
|
1322
|
+
}
|
|
1302
1323
|
if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1303
1324
|
sinceCheck = 0;
|
|
1304
1325
|
throwIfAborted(resolved.signal);
|
|
1305
1326
|
}
|
|
1306
1327
|
}
|
|
1307
1328
|
parser.finish();
|
|
1329
|
+
if (rest !== null) {
|
|
1330
|
+
report.warning(
|
|
1331
|
+
"unsupported",
|
|
1332
|
+
PAJEK_ISSUE.MULTIPLE_GRAPHS,
|
|
1333
|
+
`the project file holds ${rest.count} more network(s) after the first; import() reads the first, importAll() reads every one`,
|
|
1334
|
+
{ line: restLine },
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1308
1337
|
throwIfAborted(resolved.signal);
|
|
1309
1338
|
return report.finish();
|
|
1310
1339
|
},
|
|
1340
|
+
|
|
1341
|
+
/**
|
|
1342
|
+
* Read every network of a Pajek project file (`.paj`), each into its own sink. A network starts
|
|
1343
|
+
* at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section.
|
|
1344
|
+
* @param input - the text, bytes or stream
|
|
1345
|
+
* @param sinkFor - the sink of the network with this index, called before its first push
|
|
1346
|
+
* @param options - common and Pajek options
|
|
1347
|
+
* @returns one report per network
|
|
1348
|
+
*/
|
|
1349
|
+
async importAll(
|
|
1350
|
+
input: ImportInput,
|
|
1351
|
+
sinkFor: (index: number) => GraphSink,
|
|
1352
|
+
options?: PajekImportOptions & CommonImportOptions,
|
|
1353
|
+
): Promise<ImportReport[]> {
|
|
1354
|
+
const resolved = resolveImportOptions(options, DEFAULTS);
|
|
1355
|
+
const firstVertex = firstVertexOption(options?.firstVertex);
|
|
1356
|
+
const reports = [new ImportReportBuilder("pajek", resolved.errorLimit)];
|
|
1357
|
+
reportUnusedOptions(options, reports[0], USED_OPTIONS);
|
|
1358
|
+
const open = (): PajekParser => {
|
|
1359
|
+
const report = reports[reports.length - 1];
|
|
1360
|
+
const sink = sinkFor(reports.length - 1);
|
|
1361
|
+
reportSinkOptions(sink, options, report);
|
|
1362
|
+
return new PajekParser(sink, report, resolved, firstVertex);
|
|
1363
|
+
};
|
|
1364
|
+
let parser = open();
|
|
1365
|
+
// decoding issues belong to the file, so they go to the first network's report
|
|
1366
|
+
const reader = new LineReader(input, reports[0], resolved);
|
|
1367
|
+
let sinceCheck = 0;
|
|
1368
|
+
for await (const text of reader) {
|
|
1369
|
+
if (parser.line(text, reader.line)) {
|
|
1370
|
+
parser.finish();
|
|
1371
|
+
reports.push(new ImportReportBuilder("pajek", resolved.errorLimit));
|
|
1372
|
+
parser = open();
|
|
1373
|
+
parser.line(text, reader.line);
|
|
1374
|
+
}
|
|
1375
|
+
if (++sinceCheck >= ABORT_CHECK_INTERVAL) {
|
|
1376
|
+
sinceCheck = 0;
|
|
1377
|
+
throwIfAborted(resolved.signal);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
parser.finish();
|
|
1381
|
+
throwIfAborted(resolved.signal);
|
|
1382
|
+
return reports.map((r) => r.finish());
|
|
1383
|
+
},
|
|
1311
1384
|
});
|
|
1385
|
+
|
|
1386
|
+
/**
|
|
1387
|
+
* Counts the networks of the rest of a project file by their headers alone, with the parser's
|
|
1388
|
+
* rule: a network starts at `*Network` after a named or populated one, or at a `*Vertices` after a
|
|
1389
|
+
* populated one unless it belongs to a skipped section (`*Partition`, `*Vector`, ...).
|
|
1390
|
+
*/
|
|
1391
|
+
class NetworkCounter {
|
|
1392
|
+
/** Networks started so far (the first header fed opens the first). */
|
|
1393
|
+
count = 0;
|
|
1394
|
+
|
|
1395
|
+
private named = false;
|
|
1396
|
+
|
|
1397
|
+
private populated = false;
|
|
1398
|
+
|
|
1399
|
+
private skipping = false;
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Feed one line.
|
|
1403
|
+
* @param text - the line
|
|
1404
|
+
*/
|
|
1405
|
+
line(text: string): void {
|
|
1406
|
+
if (!isSectionLine(text)) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const h = parseSectionHeader(text);
|
|
1410
|
+
const kind = h === null ? "unsupported" : h.kind;
|
|
1411
|
+
const { skipping } = this;
|
|
1412
|
+
this.skipping = kind === "unsupported" || (kind === "vertices" && skipping && this.populated);
|
|
1413
|
+
if (kind === "network") {
|
|
1414
|
+
if (this.count === 0 || this.named || this.populated) {
|
|
1415
|
+
this.count++;
|
|
1416
|
+
this.populated = false;
|
|
1417
|
+
}
|
|
1418
|
+
this.named = true;
|
|
1419
|
+
} else if (kind === "vertices" && !this.skipping) {
|
|
1420
|
+
if (this.count === 0 || this.populated) {
|
|
1421
|
+
this.count++;
|
|
1422
|
+
this.named = false;
|
|
1423
|
+
}
|
|
1424
|
+
this.populated = true;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
package/src/index.ts
CHANGED
package/src/registry.ts
CHANGED
|
@@ -214,56 +214,84 @@ export class FormatRegistry {
|
|
|
214
214
|
* own a sink). The format is the one named in the options, else sniffed from the filename,
|
|
215
215
|
* the MIME type and the first bytes of the content; the builder is seeded from the common
|
|
216
216
|
* options with `directed: true` as a placeholder that the importer overrides from the file.
|
|
217
|
+
* An input holding several graphs yields the first, with a warning naming how many were
|
|
218
|
+
* skipped; importAllGraphs() returns every one.
|
|
217
219
|
* @param input - the text, bytes, stream or chunks to read
|
|
218
220
|
* @param options - the format, hints, common and format-specific import options
|
|
219
221
|
* @returns the snapshot, the import report and the freeze report
|
|
220
222
|
*/
|
|
221
223
|
async importGraph(input: ImportInput, options: ImportGraphOptions = {}): Promise<ImportGraphResult> {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
let sniff: SniffResult | null = null;
|
|
225
|
-
let source = input;
|
|
226
|
-
let peeked: PeekedInput | null = null;
|
|
227
|
-
if (requested === "auto") {
|
|
228
|
-
peeked = await peekHead(input, SNIFF_HEAD_BYTES, options.signal ?? null);
|
|
229
|
-
source = peeked.input;
|
|
230
|
-
sniff = this.sniff({ filename: options.filename, mimeType: options.mimeType, head: peeked.head });
|
|
231
|
-
if (sniff === null) {
|
|
232
|
-
const report = new ImportReportBuilder("unknown", 0);
|
|
233
|
-
return report.fail(
|
|
234
|
-
UNKNOWN_FORMAT_CODE,
|
|
235
|
-
`no registered importer recognises the input${describeHints(options)}; pass the format explicitly`,
|
|
236
|
-
undefined,
|
|
237
|
-
{ formats: this.formats() },
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
importer = this.importer(sniff.format);
|
|
241
|
-
} else {
|
|
242
|
-
importer = this.importer(requested);
|
|
243
|
-
}
|
|
244
|
-
const builder = new GraphBuilder({
|
|
245
|
-
weightDtype: options.weightDtype ?? "f64",
|
|
246
|
-
...options.builder,
|
|
247
|
-
directed: true,
|
|
248
|
-
addMissingNodes: options.addMissingNodes ?? true,
|
|
249
|
-
duplicateEdges: options.duplicateEdges ?? "keep",
|
|
250
|
-
selfLoops: options.selfLoops ?? "keep",
|
|
251
|
-
});
|
|
224
|
+
const chosen = await this.choose(input, options);
|
|
225
|
+
const builder = seededBuilder(options);
|
|
252
226
|
let report: ImportReport;
|
|
253
227
|
try {
|
|
254
|
-
report = await importer.import(source, builder, importerOptions(options));
|
|
228
|
+
report = await chosen.importer.import(chosen.source, builder, importerOptions(options));
|
|
255
229
|
} catch (err) {
|
|
256
|
-
await peeked?.close();
|
|
230
|
+
await chosen.peeked?.close();
|
|
257
231
|
throw err;
|
|
258
232
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
233
|
+
return result(chosen, builder, report, options);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Read every graph of an input (a DOT file with several graphs, a Pajek project with several
|
|
238
|
+
* networks, a JGF document with a `graphs` array), each into its own fresh builder, frozen.
|
|
239
|
+
* A format whose importer has no importAll() holds one graph per input, so the result has one
|
|
240
|
+
* entry. Options, sniffing and the builder seed are those of importGraph().
|
|
241
|
+
* @param input - the text, bytes, stream or chunks to read
|
|
242
|
+
* @param options - the format, hints, common and format-specific import options
|
|
243
|
+
* @returns one result per graph, in document order
|
|
244
|
+
*/
|
|
245
|
+
async importAllGraphs(input: ImportInput, options: ImportGraphOptions = {}): Promise<ImportGraphResult[]> {
|
|
246
|
+
const chosen = await this.choose(input, options);
|
|
247
|
+
const { importer } = chosen;
|
|
248
|
+
const builders: GraphBuilder[] = [];
|
|
249
|
+
let reports: ImportReport[];
|
|
250
|
+
try {
|
|
251
|
+
if (importer.importAll === undefined) {
|
|
252
|
+
builders.push(seededBuilder(options));
|
|
253
|
+
reports = [await importer.import(chosen.source, builders[0], importerOptions(options))];
|
|
254
|
+
} else {
|
|
255
|
+
reports = await importer.importAll(
|
|
256
|
+
chosen.source,
|
|
257
|
+
() => {
|
|
258
|
+
const builder = seededBuilder(options);
|
|
259
|
+
builders.push(builder);
|
|
260
|
+
return builder;
|
|
261
|
+
},
|
|
262
|
+
importerOptions(options),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} catch (err) {
|
|
266
|
+
await chosen.peeked?.close();
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
return reports.map((report, i) => result(chosen, builders[i], report, options));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The importer for an input: the named format, or the sniffed one.
|
|
274
|
+
* @param input - the input
|
|
275
|
+
* @param options - the importGraph options
|
|
276
|
+
* @returns the importer, the sniff, and the input to read (replayed for a stream)
|
|
277
|
+
*/
|
|
278
|
+
private async choose(input: ImportInput, options: ImportGraphOptions): Promise<ChosenImporter> {
|
|
279
|
+
const requested = options.format ?? "auto";
|
|
280
|
+
if (requested !== "auto") {
|
|
281
|
+
return { importer: this.importer(requested), sniff: null, source: input, peeked: null };
|
|
282
|
+
}
|
|
283
|
+
const peeked = await peekHead(input, SNIFF_HEAD_BYTES, options.signal ?? null);
|
|
284
|
+
const sniff = this.sniff({ filename: options.filename, mimeType: options.mimeType, head: peeked.head });
|
|
285
|
+
if (sniff === null) {
|
|
286
|
+
const report = new ImportReportBuilder("unknown", 0);
|
|
287
|
+
return report.fail(
|
|
288
|
+
UNKNOWN_FORMAT_CODE,
|
|
289
|
+
`no registered importer recognises the input${describeHints(options)}; pass the format explicitly`,
|
|
290
|
+
undefined,
|
|
291
|
+
{ formats: this.formats() },
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
return { importer: this.importer(sniff.format), sniff, source: peeked.input, peeked };
|
|
267
295
|
}
|
|
268
296
|
|
|
269
297
|
/**
|
|
@@ -338,6 +366,16 @@ export function importGraph(input: ImportInput, options?: ImportGraphOptions): P
|
|
|
338
366
|
return registry.importGraph(input, options);
|
|
339
367
|
}
|
|
340
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Read every graph of an input, each into its own frozen snapshot, through the default registry.
|
|
371
|
+
* @param input - the text, bytes, stream or chunks to read
|
|
372
|
+
* @param options - the format, hints, common and format-specific import options
|
|
373
|
+
* @returns one result per graph, in document order
|
|
374
|
+
*/
|
|
375
|
+
export function importAllGraphs(input: ImportInput, options?: ImportGraphOptions): Promise<ImportGraphResult[]> {
|
|
376
|
+
return registry.importAllGraphs(input, options);
|
|
377
|
+
}
|
|
378
|
+
|
|
341
379
|
/**
|
|
342
380
|
* Write a snapshot in a format through the default registry, as UTF-8 chunks.
|
|
343
381
|
* @param snapshot - the snapshot
|
|
@@ -407,6 +445,59 @@ function importerOptions(options: ImportGraphOptions): CommonImportOptions {
|
|
|
407
445
|
return out;
|
|
408
446
|
}
|
|
409
447
|
|
|
448
|
+
/** The importer chosen for an input, and the input to hand it. */
|
|
449
|
+
interface ChosenImporter {
|
|
450
|
+
/** The importer. */
|
|
451
|
+
readonly importer: GraphImporter;
|
|
452
|
+
/** The sniff that chose it, or null when the caller named the format. */
|
|
453
|
+
readonly sniff: SniffResult | null;
|
|
454
|
+
/** The input to read: the original, or a replaying iterable for a peeked stream. */
|
|
455
|
+
readonly source: ImportInput;
|
|
456
|
+
/** The peeked head of a sniffed input, or null. */
|
|
457
|
+
readonly peeked: PeekedInput | null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* A fresh builder seeded from the common options, `directed: true` as a placeholder the importer
|
|
462
|
+
* overrides from the file.
|
|
463
|
+
* @param options - the importGraph options
|
|
464
|
+
* @returns the builder
|
|
465
|
+
*/
|
|
466
|
+
function seededBuilder(options: ImportGraphOptions): GraphBuilder {
|
|
467
|
+
return new GraphBuilder({
|
|
468
|
+
weightDtype: options.weightDtype ?? "f64",
|
|
469
|
+
...options.builder,
|
|
470
|
+
directed: true,
|
|
471
|
+
addMissingNodes: options.addMissingNodes ?? true,
|
|
472
|
+
duplicateEdges: options.duplicateEdges ?? "keep",
|
|
473
|
+
selfLoops: options.selfLoops ?? "keep",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Freeze an imported builder into an importGraph result.
|
|
479
|
+
* @param chosen - the importer and its sniff
|
|
480
|
+
* @param builder - the filled builder
|
|
481
|
+
* @param report - the importer's report
|
|
482
|
+
* @param options - the importGraph options
|
|
483
|
+
* @returns the frozen result
|
|
484
|
+
*/
|
|
485
|
+
function result(
|
|
486
|
+
chosen: ChosenImporter,
|
|
487
|
+
builder: GraphBuilder,
|
|
488
|
+
report: ImportReport,
|
|
489
|
+
options: ImportGraphOptions,
|
|
490
|
+
): ImportGraphResult {
|
|
491
|
+
const frozen = builder.freezeWithReport(options.freeze);
|
|
492
|
+
return Object.freeze({
|
|
493
|
+
format: chosen.importer.format,
|
|
494
|
+
sniff: chosen.sniff,
|
|
495
|
+
snapshot: frozen.snapshot,
|
|
496
|
+
report,
|
|
497
|
+
freeze: frozen.report,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
410
501
|
/**
|
|
411
502
|
* The E_UNSUPPORTED error of a format name nothing is registered under.
|
|
412
503
|
* @param kind - importer or exporter
|
package/src/sniff.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function normalizeMimeType(mimeType: string): string {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* The head as bytes for the importers' sniff functions: at most SNIFF_HEAD_BYTES, a string
|
|
101
|
-
* encoded as UTF-8.
|
|
101
|
+
* encoded as UTF-8, a UTF-16 head with a byte order mark transcoded to UTF-8.
|
|
102
102
|
* @param head - the head as given
|
|
103
103
|
* @returns the bytes
|
|
104
104
|
*/
|
|
@@ -106,6 +106,11 @@ export function headBytes(head: Uint8Array | string): Uint8Array {
|
|
|
106
106
|
if (typeof head === "string") {
|
|
107
107
|
return new TextEncoder().encode(head.slice(0, SNIFF_HEAD_BYTES)).subarray(0, SNIFF_HEAD_BYTES);
|
|
108
108
|
}
|
|
109
|
+
if (head.byteLength >= 2 && ((head[0] === 0xff && head[1] === 0xfe) || (head[0] === 0xfe && head[1] === 0xff))) {
|
|
110
|
+
// UTF-16 with a BOM (Excel's "Unicode text"): the sniffers read UTF-8, so transcode the head
|
|
111
|
+
const encoding = head[0] === 0xff ? "utf-16le" : "utf-16be";
|
|
112
|
+
return headBytes(new TextDecoder(encoding).decode(head.subarray(0, SNIFF_HEAD_BYTES)));
|
|
113
|
+
}
|
|
109
114
|
return head.byteLength > SNIFF_HEAD_BYTES ? head.subarray(0, SNIFF_HEAD_BYTES) : head;
|
|
110
115
|
}
|
|
111
116
|
|
package/src/types.ts
CHANGED
|
@@ -19,9 +19,10 @@ import {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* What an importer reads (design section 8.4): whole text, whole bytes, a byte stream (a browser
|
|
22
|
-
* `File.stream()`, a fetch body) or an async iterable of text or byte chunks. Bytes are decoded
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* `File.stream()`, a fetch body) or an async iterable of text or byte chunks. Bytes are decoded
|
|
23
|
+
* strictly in the encoding the `encoding` option, a byte order mark or the file's own declaration
|
|
24
|
+
* names, else as UTF-8 (undeclared bytes that are not UTF-8 are read as windows-1252 with a
|
|
25
|
+
* warning), so an invalid sequence is a parse-error and never a silent U+FFFD that could alias two ids.
|
|
25
26
|
*/
|
|
26
27
|
export type ImportInput = string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<string | Uint8Array>;
|
|
27
28
|
|
|
@@ -65,6 +66,13 @@ export interface CommonImportOptions {
|
|
|
65
66
|
signal?: AbortSignal | undefined;
|
|
66
67
|
/** Progress in bytes; `bytesTotal` is known for in-memory input only. */
|
|
67
68
|
onProgress?: ((bytesDone: number, bytesTotal?: number) => void) | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The character encoding of byte input (a WHATWG label such as "utf-8", "windows-1252",
|
|
71
|
+
* "iso-8859-1", "utf-16le"); overrides the byte order mark and any encoding the file declares.
|
|
72
|
+
* Absent: a BOM decides, else the declaration (XML prolog, DOT `charset`), else UTF-8, and
|
|
73
|
+
* bytes that are not valid UTF-8 are read as windows-1252 with a warning. Ignored for text input.
|
|
74
|
+
*/
|
|
75
|
+
encoding?: string | undefined;
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
/**
|
|
@@ -94,6 +102,21 @@ export interface GraphImporter<Opts = unknown> {
|
|
|
94
102
|
* @returns the import report
|
|
95
103
|
*/
|
|
96
104
|
import(input: ImportInput, sink: GraphSink, options?: Opts & CommonImportOptions): Promise<ImportReport>;
|
|
105
|
+
/**
|
|
106
|
+
* Read every graph of an input that can hold several (a DOT file with several graphs, a
|
|
107
|
+
* Pajek project with several networks, a JGF `graphs` array), each into its own sink. An
|
|
108
|
+
* importer without this method reads one graph per input. `import()` reads the first graph
|
|
109
|
+
* and warns how many it skipped.
|
|
110
|
+
* @param input - the text, bytes or stream to read
|
|
111
|
+
* @param sinkFor - called once per graph, in document order, before that graph's first push
|
|
112
|
+
* @param options - format-specific and common options
|
|
113
|
+
* @returns one report per graph, in document order
|
|
114
|
+
*/
|
|
115
|
+
importAll?(
|
|
116
|
+
input: ImportInput,
|
|
117
|
+
sinkFor: (index: number) => GraphSink,
|
|
118
|
+
options?: Opts & CommonImportOptions,
|
|
119
|
+
): Promise<ImportReport[]>;
|
|
97
120
|
}
|
|
98
121
|
|
|
99
122
|
/**
|