@graphty/graph-io 0.3.4 → 0.3.5

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 (50) hide show
  1. package/dist/chunks/{escape-DXbOoLKl.js → escape-BjmIaFFo.js} +2 -2
  2. package/dist/chunks/{escape-DXbOoLKl.js.map → escape-BjmIaFFo.js.map} +1 -1
  3. package/dist/chunks/{importer-B9qF4e32.js → importer-BddBW3sf.js} +2 -2
  4. package/dist/chunks/{importer-B9qF4e32.js.map → importer-BddBW3sf.js.map} +1 -1
  5. package/dist/chunks/{importer-xoHpRG5x.js → importer-BtHFICnf.js} +2 -2
  6. package/dist/chunks/{importer-xoHpRG5x.js.map → importer-BtHFICnf.js.map} +1 -1
  7. package/dist/chunks/{importer-C2caVvl6.js → importer-DfI8POXb.js} +272 -63
  8. package/dist/chunks/importer-DfI8POXb.js.map +1 -0
  9. package/dist/csv.js +1 -1
  10. package/dist/dot.js +1 -1
  11. package/dist/gexf.js +1 -1
  12. package/dist/gml.js +1 -1
  13. package/dist/graph-io.js +9 -8
  14. package/dist/graphml.js +1 -1
  15. package/dist/neo4j.js +51 -16
  16. package/dist/neo4j.js.map +1 -1
  17. package/dist/pajek.js +1 -1
  18. package/dist/src/common/escape.d.ts +4 -3
  19. package/dist/src/common/escape.d.ts.map +1 -1
  20. package/dist/src/common/escape.js +5 -4
  21. package/dist/src/common/escape.js.map +1 -1
  22. package/dist/src/formats/neo4j/exporter.d.ts +3 -1
  23. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  24. package/dist/src/formats/neo4j/exporter.js +34 -6
  25. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  26. package/dist/src/formats/neo4j/importer.d.ts +16 -3
  27. package/dist/src/formats/neo4j/importer.d.ts.map +1 -1
  28. package/dist/src/formats/neo4j/importer.js +49 -11
  29. package/dist/src/formats/neo4j/importer.js.map +1 -1
  30. package/dist/src/formats/neo4j/index.d.ts +3 -3
  31. package/dist/src/formats/neo4j/index.d.ts.map +1 -1
  32. package/dist/src/formats/neo4j/index.js +3 -3
  33. package/dist/src/formats/neo4j/index.js.map +1 -1
  34. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  35. package/dist/src/formats/pajek/exporter.js +3 -2
  36. package/dist/src/formats/pajek/exporter.js.map +1 -1
  37. package/dist/src/formats/pajek/importer.d.ts +14 -8
  38. package/dist/src/formats/pajek/importer.d.ts.map +1 -1
  39. package/dist/src/formats/pajek/importer.js +233 -61
  40. package/dist/src/formats/pajek/importer.js.map +1 -1
  41. package/dist/src/formats/pajek/syntax.d.ts +39 -8
  42. package/dist/src/formats/pajek/syntax.d.ts.map +1 -1
  43. package/dist/src/formats/pajek/syntax.js +81 -11
  44. package/dist/src/formats/pajek/syntax.js.map +1 -1
  45. package/dist/src/index.d.ts +1 -1
  46. package/dist/src/index.d.ts.map +1 -1
  47. package/dist/src/index.js +1 -1
  48. package/dist/src/index.js.map +1 -1
  49. package/package.json +2 -2
  50. package/dist/chunks/importer-C2caVvl6.js.map +0 -1
@@ -11,10 +11,12 @@
11
11
  * mixed-direction policy; the third column of a line is the weight unless `weightFrom` says
12
12
  * otherwise; `*Arcslist` / `*Edgeslist` adjacency lists and `*Matrix` rows are read too; time
13
13
  * interval tokens `[1-5,7-*]` become the spells role. A second network in one file is refused
14
- * with an issue; project-file sections (`*Partition`, `*Vector`, ...) are reported as unsupported.
14
+ * with an issue. A project file's `*Partition` and `*Vector` objects become the node columns
15
+ * `partition` (i32) and `vector` (f64), wherever they stand in the file; other project-file
16
+ * sections (`*Events`, `*Permutation`, ...) are skipped with a warning.
15
17
  */
16
18
  import { GraphFormatError, INVALID_INDEX, } from "@graphty/graph-format";
17
- import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE } from "../../common/attributes.js";
19
+ import { declareResolved, RENAMED_CODE, ROLE_TAKEN_CODE, uniqueColumnName } from "../../common/attributes.js";
18
20
  import { DUPLICATE_NODE_CODE, ENCODING_FALLBACK_CODE, INVALID_ENCODING_CODE, INVALID_UTF8_CODE, MULTIPLE_GRAPHS_CODE, OPTION_IGNORED_CODE, SYNTAX_CODE, UNKNOWN_ENCODING_CODE, } from "../../common/codes.js";
19
21
  import { DirectionResolver } from "../../common/direction.js";
20
22
  import { coerceIdText, ID_MERGED_CODE, IdCoercer } from "../../common/ids.js";
@@ -23,7 +25,7 @@ import { reportSinkOptions, reportUnusedOptions, resolveImportOptions, SINK_OPTI
23
25
  import { ImportReportBuilder } from "../../common/report.js";
24
26
  import { isNumericText, TextCellWriter, WIDENING_UNSUPPORTED_CODE } from "../../common/text.js";
25
27
  import { isWeightField, parseWeightText } from "../../common/weights.js";
26
- import { isCommentOrBlank, isIntervalToken, isSectionLine, isVertexNumber, LABEL_COLUMN, ORIGINAL_ID_KEY, parseIntervals, parseSectionHeader, POSITION_COLUMN, RELATION_COLUMN, SHAPE_COLUMN, SHAPES, SPELLS_COLUMN, tokenize, VALUE_COLUMN, VALUE_FIELD, } from "./syntax.js";
28
+ import { decodeCharacterReferences, isCommentOrBlank, isIntervalToken, isSectionLine, isShapeKeyword, isVertexNumber, LABEL_COLUMN, ORIGINAL_ID_KEY, parseIntervals, parseSectionHeader, PARTITION_COLUMN, POSITION_COLUMN, RELATION_COLUMN, SHAPE_COLUMN, SPELLS_COLUMN, tokenize, VALUE_COLUMN, VALUE_FIELD, VECTOR_COLUMN, } from "./syntax.js";
27
29
  /** Issue codes of the Pajek importer. */
28
30
  export const PAJEK_ISSUE = Object.freeze({
29
31
  /** The input holds invalid UTF-8 (fatal). */
@@ -36,7 +38,7 @@ export const PAJEK_ISSUE = Object.freeze({
36
38
  UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
37
39
  /** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
38
40
  NO_VERTICES: "E_PAJEK_NO_VERTICES",
39
- /** Fatal: `*Vertices` without a vertex count, or one the sink cannot hold. */
41
+ /** Fatal: `*Vertices` without a vertex count, one the sink cannot hold, or a first-mode count outside 0..N. */
40
42
  VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
41
43
  /** A project file holds several networks; import() reads the first, importAll() reads every one. */
42
44
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
@@ -50,20 +52,24 @@ export const PAJEK_ISSUE = Object.freeze({
50
52
  VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
51
53
  /** A vertex number outside the declared range. */
52
54
  VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
53
- /** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
54
- VERTEX_COUNT: "E_PAJEK_VERTEX_COUNT",
55
+ /** Fewer vertex lines than `*Vertices` declares, which the Pajek manual allows (vertices without a line have no label). */
56
+ VERTEX_COUNT: "W_PAJEK_VERTEX_COUNT",
55
57
  /** A second line for the same vertex; the later values overwrite. */
56
58
  DUPLICATE_NODE: DUPLICATE_NODE_CODE,
57
- /** A line (arc, edge, list or matrix row) the grammar does not accept. */
59
+ /** A line (arc, edge, list, matrix row, partition or vector value) the grammar does not accept. */
58
60
  LINE: "E_PAJEK_LINE",
59
61
  /** A line endpoint outside the declared vertex range (the core's code, forwarded). */
60
62
  UNKNOWN_NODE: "E_UNKNOWN_NODE",
61
63
  /** A malformed time interval token. */
62
64
  INTERVAL: "E_PAJEK_INTERVAL",
63
- /** A `*Matrix` section with the wrong number of rows. */
65
+ /** A `*Matrix` section with the wrong number of rows (N, or N1 in a two-mode network). */
64
66
  MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
65
- /** A project-file section (`*Partition`, `*Vector`, ...) the importer does not read. */
66
- UNSUPPORTED_SECTION: "E_PAJEK_UNSUPPORTED_SECTION",
67
+ /** `*Matrix` rows longer than the column count; the extra values are ignored. */
68
+ MATRIX_EXTRA: "W_PAJEK_MATRIX_EXTRA",
69
+ /** A `*Partition` or `*Vector` whose value count differs from the network's vertex count; values beyond it are dropped, vertices without one are unset. */
70
+ OBJECT_COUNT: "E_PAJEK_OBJECT_COUNT",
71
+ /** A project-file section (`*Events`, `*Permutation`, ...) the importer does not read; its lines are skipped. */
72
+ UNSUPPORTED_SECTION: "W_PAJEK_UNSUPPORTED_SECTION",
67
73
  /** Tokens after a section header the grammar does not account for. */
68
74
  HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
69
75
  /** The file declares vertices but no line section. */
@@ -176,6 +182,18 @@ class LineError extends Error {
176
182
  this.code = code;
177
183
  }
178
184
  }
185
+ /**
186
+ * Whether a number is an integer the i32 partition column can hold.
187
+ * @param value - the number
188
+ * @returns true for an i32 integer
189
+ */
190
+ function isI32(value) {
191
+ return Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;
192
+ }
193
+ /** A vertex number of an adjacency list: Pajek reads a negative one as its absolute value. */
194
+ const LIST_VERTEX = /^-?[0-9]+$/;
195
+ /** The relation prefix `k:` of a line in a line section. */
196
+ const RELATION_PREFIX = /^([0-9]+):$/;
179
197
  /**
180
198
  * One import call's state machine: the sections, the vertex range, the columns declared so far
181
199
  * and the direction resolver.
@@ -204,6 +222,16 @@ class PajekParser {
204
222
  this.nodesCreated = false;
205
223
  this.verticesDone = false;
206
224
  this.sawLineSection = false;
225
+ /** The direction of the first line section, for a network whose line sections are all empty. */
226
+ this.firstKind = "directed";
227
+ /** Set once the first line has fixed the sink's direction. */
228
+ this.directionSet = false;
229
+ /** The names of the numbered relations (`*Arcs :k "name"`), for the `k:` line prefix. */
230
+ this.relationNames = new Map();
231
+ /** The `*Partition` / `*Vector` objects read so far, written by finish(). */
232
+ this.objects = [];
233
+ /** Set when the previous line was a project-object header, whose `*Vertices N` line comes next. */
234
+ this.objectHeader = false;
207
235
  /** Node index per vertex position (vertex number minus base). */
208
236
  this.indexOfPos = new Uint32Array(0);
209
237
  /**
@@ -251,9 +279,11 @@ class PajekParser {
251
279
  if (isCommentOrBlank(text)) {
252
280
  return false;
253
281
  }
282
+ const afterObjectHeader = this.objectHeader;
283
+ this.objectHeader = false;
254
284
  try {
255
285
  if (isSectionLine(text)) {
256
- this.header(text, line);
286
+ this.header(text, line, afterObjectHeader);
257
287
  return this.ended;
258
288
  }
259
289
  switch (this.section) {
@@ -271,6 +301,9 @@ class PajekParser {
271
301
  case "matrix":
272
302
  this.matrixLine(text, line);
273
303
  return false;
304
+ case "object":
305
+ this.objectLine(text);
306
+ return false;
274
307
  case "skip":
275
308
  return false;
276
309
  default: {
@@ -302,24 +335,69 @@ class PajekParser {
302
335
  this.report.warning("validation-error", PAJEK_ISSUE.NO_LINES, "the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section");
303
336
  this.resolver.setHeader(this.options.defaultDirected);
304
337
  }
338
+ else if (!this.directionSet) {
339
+ // every line section was empty: the first header still says which way the network goes
340
+ this.resolver.setHeader(this.firstKind === "directed");
341
+ }
342
+ const objects = this.writeObjects();
305
343
  if (this.coercer.mergeCount > 0) {
306
344
  this.report.warning("coercion", PAJEK_ISSUE.ID_MERGED, `${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`);
307
345
  }
346
+ const pajek = {
347
+ ...(this.firstMode === null ? {} : { firstMode: this.firstMode }),
348
+ ...(objects.length === 0 ? {} : { objects }),
349
+ };
308
350
  const meta = {
309
351
  sourceFormat: "pajek",
310
352
  ...(this.networkName === null ? {} : { name: this.networkName }),
311
- ...(this.firstMode === null ? {} : { extra: { pajek: { firstMode: this.firstMode } } }),
353
+ ...(Object.keys(pajek).length === 0 ? {} : { extra: { pajek } }),
312
354
  };
313
355
  this.sink.setMeta(meta);
314
356
  }
357
+ /**
358
+ * Write every `*Partition` / `*Vector` object into its node column: `partition` (i32) or
359
+ * `vector` (f64), a later object of the same kind in `partition#2`, `vector#2`, ... The value
360
+ * of vertex k is the object's k-th value.
361
+ * @returns the objects' Pajek names and the columns they went to, for meta.extra.pajek.objects
362
+ */
363
+ writeObjects() {
364
+ const used = new Set();
365
+ const written = [];
366
+ for (const object of this.objects) {
367
+ const where = { line: object.line };
368
+ const partition = object.kind === "partition";
369
+ const name = uniqueColumnName(partition ? PARTITION_COLUMN : VECTOR_COLUMN, null, (n) => used.has(n));
370
+ used.add(name);
371
+ const { handle, decl } = declareResolved(this.sink, "node", {
372
+ name,
373
+ dtype: partition ? "i32" : "f64",
374
+ nullable: true,
375
+ origin: { format: "pajek", id: object.kind },
376
+ }, this.report, where);
377
+ const n = Math.min(object.values.length, this.vertexCount);
378
+ for (let pos = 0; pos < n; pos++) {
379
+ const value = object.values[pos];
380
+ if (value !== null) {
381
+ this.sink.setNodeValue(handle, this.indexOfPos[pos], value);
382
+ }
383
+ }
384
+ const declared = object.count ?? object.values.length;
385
+ if (declared !== this.vertexCount || object.values.length !== declared) {
386
+ this.report.error("validation-error", PAJEK_ISSUE.OBJECT_COUNT, `*${partition ? "Partition" : "Vector"} "${object.name}" declares ${declared} vertices and holds ${object.values.length} value(s); the network has ${this.vertexCount}`, where);
387
+ }
388
+ written.push({ kind: object.kind, name: object.name, column: decl.name });
389
+ }
390
+ return written;
391
+ }
315
392
  // ============================================================ headers
316
393
  /**
317
394
  * A section header line.
318
395
  * @param text - the line
319
396
  * @param line - the line number
397
+ * @param afterObjectHeader - the previous line was a `*Partition`, `*Vector` or skipped section header
320
398
  */
321
- header(text, line) {
322
- const skipping = this.section === "skip";
399
+ header(text, line, afterObjectHeader) {
400
+ const previous = this.section;
323
401
  this.endSection();
324
402
  const h = parseSectionHeader(text);
325
403
  if (h === null) {
@@ -339,9 +417,13 @@ class PajekParser {
339
417
  this.section = "none";
340
418
  return;
341
419
  case "vertices":
342
- if (skipping && this.vertexCount >= 0) {
343
- // the `*Vertices N` line of a skipped project-file section (*Partition, *Vector)
344
- this.section = "skip";
420
+ if (afterObjectHeader) {
421
+ // the `*Vertices N` line of a project object (*Partition, *Vector, a skipped
422
+ // section), wherever it stands: never the network's own
423
+ this.section = previous;
424
+ if (previous === "object") {
425
+ this.objects[this.objects.length - 1].count = h.count;
426
+ }
345
427
  return;
346
428
  }
347
429
  if (this.vertexCount >= 0) {
@@ -363,9 +445,16 @@ class PajekParser {
363
445
  this.lineSection(h, line, "matrix", "directed");
364
446
  this.matrixRows = 0;
365
447
  return;
448
+ case "partition":
449
+ case "vector":
450
+ this.objects.push({ kind: h.kind, name: h.name ?? "", line, count: null, values: [] });
451
+ this.section = "object";
452
+ this.objectHeader = true;
453
+ return;
366
454
  case "unsupported":
367
455
  this.section = "skip";
368
- this.report.error("unsupported", PAJEK_ISSUE.UNSUPPORTED_SECTION, `the *${h.keyword} section is not supported; its lines are skipped`, { line });
456
+ this.objectHeader = true;
457
+ this.report.warning("unsupported", PAJEK_ISSUE.UNSUPPORTED_SECTION, `the *${h.keyword} section is not supported; its lines are skipped`, { line });
369
458
  return;
370
459
  default: {
371
460
  const name = h.kind;
@@ -385,6 +474,9 @@ class PajekParser {
385
474
  if (!Number.isSafeInteger(h.count)) {
386
475
  this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
387
476
  }
477
+ if (h.secondCount !== null && (h.secondCount < 0 || h.secondCount > h.count)) {
478
+ this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count} ${h.secondCount}: the first-mode count is not within 0..${h.count}`, { line });
479
+ }
388
480
  try {
389
481
  // the declared count must be reservable (the sink is the authority on what it can hold,
390
482
  // E_TOO_LARGE beyond MAX_COUNT) before any vertex is materialised
@@ -412,8 +504,9 @@ class PajekParser {
412
504
  }
413
505
  }
414
506
  /**
415
- * A line section header: the vertex set is completed, the first one sets the sink's direction
416
- * (rule 1 of design section 8.4), later ones only switch the kind.
507
+ * A line section header: the vertex set is completed and the kind switched. The sink's
508
+ * direction (rule 1 of design section 8.4) is set by the first LINE, not the first header, so
509
+ * an empty `*Arcs` before the `*Edges` of an undirected network does not make it directed.
417
510
  * @param h - the header
418
511
  * @param line - the line number
419
512
  * @param section - the parser state for the section
@@ -426,18 +519,22 @@ class PajekParser {
426
519
  this.finishVertices();
427
520
  if (!this.sawLineSection) {
428
521
  this.sawLineSection = true;
429
- this.resolver.setHeader(kind === "directed", { line });
522
+ this.firstKind = kind;
430
523
  }
431
524
  this.section = section;
432
525
  this.kind = kind;
433
526
  this.relation = h.relation === null ? null : (h.name ?? String(h.relation));
527
+ if (h.relation !== null) {
528
+ this.relationNames.set(h.relation, this.relation ?? String(h.relation));
529
+ }
434
530
  }
435
531
  /**
436
532
  * Checks that run when a section ends.
437
533
  */
438
534
  endSection() {
439
- if (this.section === "matrix" && this.matrixRows !== this.vertexCount) {
440
- this.report.error("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${this.vertexCount}`);
535
+ const rows = this.firstMode ?? this.vertexCount;
536
+ if (this.section === "matrix" && this.matrixRows !== rows) {
537
+ this.report.error("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${rows}`);
441
538
  }
442
539
  this.section = "none";
443
540
  }
@@ -529,7 +626,7 @@ class PajekParser {
529
626
  // a partial list: fewer lines than declared, counting the lines skipped after an error
530
627
  const lines = this.seenCount + this.report.counts.skippedNodes;
531
628
  if (this.seenCount > 0 && lines < n) {
532
- this.report.error("validation-error", PAJEK_ISSUE.VERTEX_COUNT, `*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`);
629
+ this.report.warning("validation-error", PAJEK_ISSUE.VERTEX_COUNT, `*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`);
533
630
  }
534
631
  }
535
632
  /**
@@ -554,8 +651,9 @@ class PajekParser {
554
651
  }
555
652
  // parse everything before writing anything
556
653
  let i = 1;
557
- const label = tokens.length > 1 ? tokens[1] : null;
558
- if (label !== null) {
654
+ let label = null;
655
+ if (tokens.length > 1 && !startsWithCoordinates(text, tokens)) {
656
+ label = decodeCharacterReferences(tokens[1]);
559
657
  i = 2;
560
658
  }
561
659
  let x = 0;
@@ -578,8 +676,8 @@ class PajekParser {
578
676
  throw new LineError("parse-error", PAJEK_ISSUE.VERTEX_LINE, `vertex ${k}: a single coordinate "${tokens[i]}"; coordinates are x y [z]`);
579
677
  }
580
678
  let shape = null;
581
- if (i < tokens.length && SHAPES.has(tokens[i])) {
582
- shape = tokens[i];
679
+ if (i < tokens.length && isShapeKeyword(tokens[i])) {
680
+ shape = tokens[i].toLowerCase();
583
681
  i++;
584
682
  }
585
683
  const extras = this.extras(tokens, i, "node");
@@ -732,7 +830,15 @@ class PajekParser {
732
830
  * @param line - the line number
733
831
  */
734
832
  edgeLine(text, line) {
735
- const tokens = this.tokens(text, "edge");
833
+ let tokens = this.tokens(text, "edge");
834
+ // a `k:` prefix puts this one line in relation k
835
+ let { relation } = this;
836
+ const prefix = RELATION_PREFIX.exec(tokens[0]);
837
+ if (prefix !== null) {
838
+ const k = Number(prefix[1]);
839
+ relation = this.relationNames.get(k) ?? String(k);
840
+ tokens = tokens.slice(1);
841
+ }
736
842
  if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
737
843
  this.report.counts.skippedEdges++;
738
844
  throw new LineError("parse-error", PAJEK_ISSUE.LINE, `a line starts with two vertex numbers, not "${tokens.slice(0, 2).join(" ")}"`);
@@ -768,30 +874,33 @@ class PajekParser {
768
874
  // inferred like every parameter, so a re-import of the exporter's `value` parameter agrees
769
875
  this.writeParameter("edge", VALUE_COLUMN, e, valueText);
770
876
  }
771
- this.writeEdgeExtras(e, extras);
877
+ this.writeEdgeExtras(e, extras, relation);
772
878
  }
773
879
  /**
774
- * An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`.
880
+ * An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`; a negative vertex
881
+ * number is read as its absolute value, as Pajek does.
775
882
  * @param text - the line
776
883
  * @param line - the line number
777
884
  */
778
885
  listLine(text, line) {
779
886
  const tokens = this.tokens(text, "edge");
780
887
  for (const token of tokens) {
781
- if (!isVertexNumber(token)) {
888
+ if (!LIST_VERTEX.test(token)) {
782
889
  this.report.counts.skippedEdges++;
783
890
  throw new LineError("parse-error", PAJEK_ISSUE.LINE, `an adjacency list holds vertex numbers only, not "${token}"`);
784
891
  }
785
892
  }
786
- const u = this.endpoint(Number(tokens[0]));
893
+ const u = this.endpoint(Math.abs(Number(tokens[0])));
787
894
  for (let i = 1; i < tokens.length; i++) {
788
- const v = this.endpoint(Number(tokens[i]));
895
+ const v = this.endpoint(Math.abs(Number(tokens[i])));
789
896
  const e = this.pushEdge(u, v, undefined, line);
790
897
  this.writeRelation(e);
791
898
  }
792
899
  }
793
900
  /**
794
- * A `*Matrix` row: N values, every non-zero entry an arc from the row's vertex with that value.
901
+ * A `*Matrix` row, every non-zero entry an arc from the row's vertex with that value: N values
902
+ * in a one-mode network; in a two-mode network (`*Vertices N N1`) the matrix is N1 rows of
903
+ * N - N1 values, entry j of row i an arc from vertex i to vertex N1 + j.
795
904
  * @param text - the line
796
905
  * @param line - the line number
797
906
  */
@@ -799,26 +908,51 @@ class PajekParser {
799
908
  const tokens = this.tokens(text, "edge");
800
909
  const row = this.matrixRows;
801
910
  this.matrixRows++;
802
- if (row >= this.vertexCount) {
803
- throw new LineError("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix row ${row + 1} is beyond the ${this.vertexCount} declared vertices`);
804
- }
805
- if (tokens.length !== this.vertexCount) {
806
- throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1} has ${tokens.length} value(s); *Vertices declares ${this.vertexCount}`);
807
- }
808
- for (const token of tokens) {
809
- if (!isNumericText(token)) {
810
- throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1}: "${token}" is not a number`);
911
+ const offset = this.firstMode ?? 0;
912
+ const rows = this.firstMode ?? this.vertexCount;
913
+ const columns = this.vertexCount - offset;
914
+ if (row >= rows) {
915
+ throw new LineError("validation-error", PAJEK_ISSUE.MATRIX_ROWS, `*Matrix row ${row + 1} is beyond the ${rows} declared row(s)`);
916
+ }
917
+ if (tokens.length < columns) {
918
+ throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1} has ${tokens.length} value(s); the matrix has ${columns} column(s)`);
919
+ }
920
+ for (let j = 0; j < columns; j++) {
921
+ if (!isNumericText(tokens[j])) {
922
+ throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*Matrix row ${row + 1}: "${tokens[j]}" is not a number`);
811
923
  }
812
924
  }
813
- for (let j = 0; j < tokens.length; j++) {
925
+ if (tokens.length > columns) {
926
+ this.report.warnOnce("validation-error", PAJEK_ISSUE.MATRIX_EXTRA, `*Matrix row ${row + 1} has ${tokens.length} value(s) for ${columns} column(s); the extra values are ignored`, { line });
927
+ }
928
+ for (let j = 0; j < columns; j++) {
814
929
  const value = Number(tokens[j]);
815
930
  if (value === 0) {
816
931
  continue;
817
932
  }
818
- const e = this.pushEdge(row, j, value, line);
933
+ const e = this.pushEdge(row, offset + j, value, line);
819
934
  this.writeRelation(e);
820
935
  }
821
936
  }
937
+ /**
938
+ * A value line of a `*Partition` (integers) or `*Vector` (numbers): the values of the next
939
+ * vertices, usually one per line.
940
+ * @param text - the line
941
+ */
942
+ objectLine(text) {
943
+ const object = this.objects[this.objects.length - 1];
944
+ const tokens = this.tokens(text, "node");
945
+ for (const token of tokens) {
946
+ if (!isNumericText(token) || (object.kind === "partition" && !isI32(Number(token)))) {
947
+ // keep the later values on their vertices
948
+ object.values.push(null);
949
+ throw new LineError("parse-error", PAJEK_ISSUE.LINE, `*${object.kind === "partition" ? "Partition" : "Vector"} "${object.name}": "${token}" is not ${object.kind === "partition" ? "an integer" : "a number"}`);
950
+ }
951
+ }
952
+ for (const token of tokens) {
953
+ object.values.push(Number(token));
954
+ }
955
+ }
822
956
  /**
823
957
  * Push one line through the direction resolver, counting the logical edges it produced.
824
958
  * @param u - the source position
@@ -829,6 +963,10 @@ class PajekParser {
829
963
  */
830
964
  pushEdge(u, v, weight, line) {
831
965
  const { sink } = this;
966
+ if (!this.directionSet) {
967
+ this.directionSet = true;
968
+ this.resolver.setHeader(this.kind === "directed", { line });
969
+ }
832
970
  const before = sink.edgeCount;
833
971
  const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
834
972
  this.report.counts.edges += sink.edgeCount - before;
@@ -838,10 +976,11 @@ class PajekParser {
838
976
  * Write the relation, parameters and spells of a line on its primary half.
839
977
  * @param e - the edge index
840
978
  * @param extras - the parsed extras
979
+ * @param relation - the line's relation name, or null
841
980
  */
842
- writeEdgeExtras(e, extras) {
981
+ writeEdgeExtras(e, extras, relation) {
843
982
  const { sink } = this;
844
- this.writeRelation(e);
983
+ this.writeRelation(e, relation);
845
984
  for (let p = 0; p < extras.keys.length; p++) {
846
985
  this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
847
986
  }
@@ -853,17 +992,18 @@ class PajekParser {
853
992
  }
854
993
  }
855
994
  /**
856
- * Write the current section's relation name on an edge, when the section has one.
995
+ * Write a relation name on an edge: the current section's, unless a line prefix named another.
857
996
  * @param e - the edge index
997
+ * @param relation - the relation name, or null for none
858
998
  */
859
- writeRelation(e) {
860
- if (this.relation === null) {
999
+ writeRelation(e, relation = this.relation) {
1000
+ if (relation === null) {
861
1001
  return;
862
1002
  }
863
1003
  if (this.relationHandle === INVALID_INDEX) {
864
1004
  this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
865
1005
  }
866
- this.sink.setEdgeValue(this.relationHandle, e, this.relation);
1006
+ this.sink.setEdgeValue(this.relationHandle, e, relation);
867
1007
  }
868
1008
  /**
869
1009
  * A line endpoint: the vertex number checked against the declared range; a reference outside
@@ -928,7 +1068,9 @@ class PajekParser {
928
1068
  const token = tokens[i];
929
1069
  if (isIntervalToken(token)) {
930
1070
  try {
931
- extras.spells = parseIntervals(token);
1071
+ const spells = parseIntervals(token);
1072
+ // an empty `[]` is no spell at all
1073
+ extras.spells = spells.length === 0 ? null : spells;
932
1074
  }
933
1075
  catch (err) {
934
1076
  this.skip(domain);
@@ -983,6 +1125,25 @@ class PajekParser {
983
1125
  writer.write(row, text);
984
1126
  }
985
1127
  }
1128
+ /**
1129
+ * Whether a vertex line has coordinates but no label: the token after the vertex number is bare
1130
+ * (not quoted) and starts a run of exactly two numbers, which cannot be a label and one
1131
+ * coordinate. A run of three stays a numeric label and x y (the NetworkX writer's `1 0 0.5 0.5`),
1132
+ * the one reading of it that keeps real files' labels.
1133
+ * @param text - the line
1134
+ * @param tokens - its tokens
1135
+ * @returns true when tokens[1] is the first coordinate
1136
+ */
1137
+ function startsWithCoordinates(text, tokens) {
1138
+ if (text.trimStart().slice(tokens[0].length).trimStart().startsWith('"')) {
1139
+ return false;
1140
+ }
1141
+ let run = 0;
1142
+ while (1 + run < tokens.length && isNumericText(tokens[1 + run])) {
1143
+ run++;
1144
+ }
1145
+ return run === 2;
1146
+ }
986
1147
  /**
987
1148
  * Resolve the firstVertex option.
988
1149
  * @param value - the caller's value
@@ -1061,7 +1222,8 @@ export const pajekImporter = Object.freeze({
1061
1222
  },
1062
1223
  /**
1063
1224
  * Read every network of a Pajek project file (`.paj`), each into its own sink. A network starts
1064
- * at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section.
1225
+ * at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section
1226
+ * (the `*Vertices N` line right after an object's header).
1065
1227
  * @param input - the text, bytes or stream
1066
1228
  * @param sinkFor - the sink of the network with this index, called before its first push
1067
1229
  * @param options - common and Pajek options
@@ -1102,7 +1264,8 @@ export const pajekImporter = Object.freeze({
1102
1264
  /**
1103
1265
  * Counts the networks of the rest of a project file by their headers alone, with the parser's
1104
1266
  * rule: a network starts at `*Network` after a named or populated one, or at a `*Vertices` after a
1105
- * populated one unless it belongs to a skipped section (`*Partition`, `*Vector`, ...).
1267
+ * populated one unless it is the line right after a project object's header (`*Partition`,
1268
+ * `*Vector`, a skipped section).
1106
1269
  */
1107
1270
  class NetworkCounter {
1108
1271
  constructor() {
@@ -1110,28 +1273,37 @@ class NetworkCounter {
1110
1273
  this.count = 0;
1111
1274
  this.named = false;
1112
1275
  this.populated = false;
1113
- this.skipping = false;
1276
+ this.afterObjectHeader = false;
1114
1277
  }
1115
1278
  /**
1116
1279
  * Feed one line.
1117
1280
  * @param text - the line
1118
1281
  */
1119
1282
  line(text) {
1283
+ if (isCommentOrBlank(text)) {
1284
+ return;
1285
+ }
1286
+ const { afterObjectHeader } = this;
1287
+ this.afterObjectHeader = false;
1120
1288
  if (!isSectionLine(text)) {
1121
1289
  return;
1122
1290
  }
1123
1291
  const h = parseSectionHeader(text);
1124
- const kind = h === null ? "unsupported" : h.kind;
1125
- const { skipping } = this;
1126
- this.skipping = kind === "unsupported" || (kind === "vertices" && skipping && this.populated);
1127
- if (kind === "network") {
1292
+ if (h === null) {
1293
+ return;
1294
+ }
1295
+ const { kind } = h;
1296
+ if (kind === "partition" || kind === "vector" || kind === "unsupported") {
1297
+ this.afterObjectHeader = true;
1298
+ }
1299
+ else if (kind === "network") {
1128
1300
  if (this.count === 0 || this.named || this.populated) {
1129
1301
  this.count++;
1130
1302
  this.populated = false;
1131
1303
  }
1132
1304
  this.named = true;
1133
1305
  }
1134
- else if (kind === "vertices" && !this.skipping) {
1306
+ else if (kind === "vertices" && !afterObjectHeader) {
1135
1307
  if (this.count === 0 || this.populated) {
1136
1308
  this.count++;
1137
1309
  this.named = false;