@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
@@ -1,6 +1,6 @@
1
1
  import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
2
- import { L as LOSS, ax as joinText, ak as encodeChunks, aE as resolveExportOptions, aG as sanitizeIds, ad as checkCapabilities, az as pairFolding, ac as capabilities, al as explicitWeights, ao as formatF64, am as formatDecimal, an as formatF32, aq as formatInteger, ab as canonicalId, Y as SINK_OPTION_CODE, P as OPTION_IGNORED_CODE, X as ROLE_TAKEN_CODE, U as RENAMED_CODE, o as ID_MERGED_CODE, k as DUPLICATE_NODE_CODE, $ as SYNTAX_CODE, A as MULTIPLE_GRAPHS_CODE, a5 as UNKNOWN_ENCODING_CODE, n as ENCODING_FALLBACK_CODE, s as INVALID_ENCODING_CODE, u as INVALID_UTF8_CODE, aF as resolveImportOptions, I as ImportReportBuilder, aD as reportUnusedOptions, x as LineReader, a as throwIfAborted, aC as reportSinkOptions, l as DirectionResolver, v as IdCoercer, af as coerceIdText, ai as declareResolved, aw as isWeightField, aA as parseWeightText } from "./writer-BdMak4_J.js";
3
- import { i as isPajekLabel, q as quotePajekLabel } from "./escape-DXbOoLKl.js";
2
+ import { L as LOSS, ax as joinText, ak as encodeChunks, aE as resolveExportOptions, aG as sanitizeIds, ad as checkCapabilities, az as pairFolding, ac as capabilities, al as explicitWeights, ao as formatF64, am as formatDecimal, an as formatF32, aq as formatInteger, ab as canonicalId, Y as SINK_OPTION_CODE, P as OPTION_IGNORED_CODE, X as ROLE_TAKEN_CODE, U as RENAMED_CODE, o as ID_MERGED_CODE, k as DUPLICATE_NODE_CODE, $ as SYNTAX_CODE, A as MULTIPLE_GRAPHS_CODE, a5 as UNKNOWN_ENCODING_CODE, n as ENCODING_FALLBACK_CODE, s as INVALID_ENCODING_CODE, u as INVALID_UTF8_CODE, aF as resolveImportOptions, I as ImportReportBuilder, aD as reportUnusedOptions, x as LineReader, a as throwIfAborted, aC as reportSinkOptions, l as DirectionResolver, v as IdCoercer, aL as uniqueColumnName, ai as declareResolved, af as coerceIdText, aw as isWeightField, aA as parseWeightText } from "./writer-BdMak4_J.js";
3
+ import { i as isPajekLabel, q as quotePajekLabel } from "./escape-BjmIaFFo.js";
4
4
  import { i as inferTextDtype, W as WIDENING_UNSUPPORTED_CODE, a as isNumericText, T as TextCellWriter } from "./text-Dr0Ifpag.js";
5
5
  const LABEL_COLUMN = "label";
6
6
  const POSITION_COLUMN = "position";
@@ -9,7 +9,22 @@ const RELATION_COLUMN = "relation";
9
9
  const SPELLS_COLUMN = "spells";
10
10
  const VALUE_COLUMN = "value";
11
11
  const VALUE_FIELD = "value";
12
- const SHAPES = /* @__PURE__ */ new Set(["ellipse", "box", "diamond", "triangle", "cross", "empty"]);
12
+ const PARTITION_COLUMN = "partition";
13
+ const VECTOR_COLUMN = "vector";
14
+ const SHAPES = /* @__PURE__ */ new Set([
15
+ "ellipse",
16
+ "box",
17
+ "diamond",
18
+ "triangle",
19
+ "cross",
20
+ "empty",
21
+ "house",
22
+ "man",
23
+ "woman"
24
+ ]);
25
+ function isShapeKeyword(token) {
26
+ return SHAPES.has(token.toLowerCase());
27
+ }
13
28
  const ORIGINAL_ID_KEY = "graphty_originalId";
14
29
  const SECTION_KINDS = /* @__PURE__ */ new Map([
15
30
  ["network", "network"],
@@ -18,7 +33,9 @@ const SECTION_KINDS = /* @__PURE__ */ new Map([
18
33
  ["edges", "edges"],
19
34
  ["arcslist", "arcslist"],
20
35
  ["edgeslist", "edgeslist"],
21
- ["matrix", "matrix"]
36
+ ["matrix", "matrix"],
37
+ ["partition", "partition"],
38
+ ["vector", "vector"]
22
39
  ]);
23
40
  const INTEGER_TEXT = /^[+-]?[0-9]+$/;
24
41
  const TIME_POINT = /^(\*|[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?)(?:-(\*|[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?))?$/;
@@ -34,6 +51,15 @@ function tokenize(line) {
34
51
  started = true;
35
52
  continue;
36
53
  }
54
+ if (!quoted && !started && c === 91) {
55
+ const close = line.indexOf("]", i);
56
+ if (close > i && line.lastIndexOf("[", close) === i) {
57
+ current += line.slice(i, close + 1);
58
+ started = true;
59
+ i = close;
60
+ continue;
61
+ }
62
+ }
37
63
  if (!quoted && (c === 32 || c === 9 || c === 13 || c === 12 || c === 11)) {
38
64
  if (started) {
39
65
  tokens.push(current);
@@ -112,6 +138,8 @@ function parseSectionHeader(line) {
112
138
  }
113
139
  break;
114
140
  case "network":
141
+ case "partition":
142
+ case "vector":
115
143
  if (i < tokens.length) {
116
144
  name = tokens.slice(i).join(" ");
117
145
  i = tokens.length;
@@ -148,11 +176,11 @@ function isIntervalToken(token) {
148
176
  function parseIntervals(token) {
149
177
  const body = token.slice(1, -1);
150
178
  if (body.trim().length === 0) {
151
- throw new Error(`empty time interval ${token}`);
179
+ return [];
152
180
  }
153
181
  const spells = [];
154
182
  for (const part of body.split(",")) {
155
- const match = TIME_POINT.exec(part.trim());
183
+ const match = TIME_POINT.exec(part.replace(/\s+/g, ""));
156
184
  if (match === null) {
157
185
  throw new Error(`malformed time interval ${token}: "${part}" is not a-b, a-* or a`);
158
186
  }
@@ -173,6 +201,22 @@ function parseIntervals(token) {
173
201
  }
174
202
  return spells;
175
203
  }
204
+ const CHARACTER_REFERENCE = /&#(?:[xX]([0-9a-fA-F]{1,6})|([0-9]{1,7}));/g;
205
+ function decodeCharacterReferences(text) {
206
+ if (!text.includes("&#")) {
207
+ return text;
208
+ }
209
+ return text.replace(CHARACTER_REFERENCE, (whole, hex, dec) => {
210
+ const code = hex === void 0 ? Number(dec) : Number.parseInt(hex, 16);
211
+ return code <= 1114111 ? String.fromCodePoint(code) : whole;
212
+ });
213
+ }
214
+ function encodeCharacterReferences(text) {
215
+ if (!text.includes("&#")) {
216
+ return text;
217
+ }
218
+ return text.replace(CHARACTER_REFERENCE, (whole) => `&#38;${whole.slice(1)}`);
219
+ }
176
220
  function formatIntervals(spells) {
177
221
  const parts = [];
178
222
  for (const [start, end] of spells) {
@@ -189,7 +233,7 @@ function isParameterKey(text) {
189
233
  if (text.length === 0 || /[\s"]/.test(text) || text.startsWith("[") || text.startsWith("*")) {
190
234
  return false;
191
235
  }
192
- if (SHAPES.has(text)) {
236
+ if (isShapeKeyword(text)) {
193
237
  return false;
194
238
  }
195
239
  return !/^[+-]?(\.[0-9]|[0-9])/.test(text);
@@ -741,7 +785,7 @@ function* writeVertices(snapshot, ids, nodePlan, mangle) {
741
785
  const label = labelOf(labels, ids, i, parts.length > 0);
742
786
  let line = String(i + 1);
743
787
  if (label !== null) {
744
- line += ` ${quotePajekLabel(label)}`;
788
+ line += ` ${quotePajekLabel(encodeCharacterReferences(label))}`;
745
789
  }
746
790
  if (parts.length > 0) {
747
791
  line += ` ${parts.join(" ")}`;
@@ -971,7 +1015,7 @@ const PAJEK_ISSUE = Object.freeze({
971
1015
  UNKNOWN_ENCODING: UNKNOWN_ENCODING_CODE,
972
1016
  /** Fatal: no `*Vertices` section (an empty file, or not a Pajek network). */
973
1017
  NO_VERTICES: "E_PAJEK_NO_VERTICES",
974
- /** Fatal: `*Vertices` without a vertex count, or one the sink cannot hold. */
1018
+ /** Fatal: `*Vertices` without a vertex count, one the sink cannot hold, or a first-mode count outside 0..N. */
975
1019
  VERTICES_COUNT: "E_PAJEK_VERTICES_COUNT",
976
1020
  /** A project file holds several networks; import() reads the first, importAll() reads every one. */
977
1021
  MULTIPLE_GRAPHS: MULTIPLE_GRAPHS_CODE,
@@ -985,20 +1029,24 @@ const PAJEK_ISSUE = Object.freeze({
985
1029
  VERTEX_LINE: "E_PAJEK_VERTEX_LINE",
986
1030
  /** A vertex number outside the declared range. */
987
1031
  VERTEX_RANGE: "E_PAJEK_VERTEX_RANGE",
988
- /** Fewer vertex lines than `*Vertices` declares (vertices without a line have no label). */
989
- VERTEX_COUNT: "E_PAJEK_VERTEX_COUNT",
1032
+ /** Fewer vertex lines than `*Vertices` declares, which the Pajek manual allows (vertices without a line have no label). */
1033
+ VERTEX_COUNT: "W_PAJEK_VERTEX_COUNT",
990
1034
  /** A second line for the same vertex; the later values overwrite. */
991
1035
  DUPLICATE_NODE: DUPLICATE_NODE_CODE,
992
- /** A line (arc, edge, list or matrix row) the grammar does not accept. */
1036
+ /** A line (arc, edge, list, matrix row, partition or vector value) the grammar does not accept. */
993
1037
  LINE: "E_PAJEK_LINE",
994
1038
  /** A line endpoint outside the declared vertex range (the core's code, forwarded). */
995
1039
  UNKNOWN_NODE: "E_UNKNOWN_NODE",
996
1040
  /** A malformed time interval token. */
997
1041
  INTERVAL: "E_PAJEK_INTERVAL",
998
- /** A `*Matrix` section with the wrong number of rows. */
1042
+ /** A `*Matrix` section with the wrong number of rows (N, or N1 in a two-mode network). */
999
1043
  MATRIX_ROWS: "E_PAJEK_MATRIX_ROWS",
1000
- /** A project-file section (`*Partition`, `*Vector`, ...) the importer does not read. */
1001
- UNSUPPORTED_SECTION: "E_PAJEK_UNSUPPORTED_SECTION",
1044
+ /** `*Matrix` rows longer than the column count; the extra values are ignored. */
1045
+ MATRIX_EXTRA: "W_PAJEK_MATRIX_EXTRA",
1046
+ /** A `*Partition` or `*Vector` whose value count differs from the network's vertex count; values beyond it are dropped, vertices without one are unset. */
1047
+ OBJECT_COUNT: "E_PAJEK_OBJECT_COUNT",
1048
+ /** A project-file section (`*Events`, `*Permutation`, ...) the importer does not read; its lines are skipped. */
1049
+ UNSUPPORTED_SECTION: "W_PAJEK_UNSUPPORTED_SECTION",
1002
1050
  /** Tokens after a section header the grammar does not account for. */
1003
1051
  HEADER_EXTRA: "W_PAJEK_HEADER_EXTRA",
1004
1052
  /** The file declares vertices but no line section. */
@@ -1099,6 +1147,11 @@ class LineError extends Error {
1099
1147
  this.code = code;
1100
1148
  }
1101
1149
  }
1150
+ function isI32(value) {
1151
+ return Number.isInteger(value) && value >= -2147483648 && value <= 2147483647;
1152
+ }
1153
+ const LIST_VERTEX = /^-?[0-9]+$/;
1154
+ const RELATION_PREFIX = /^([0-9]+):$/;
1102
1155
  class PajekParser {
1103
1156
  /**
1104
1157
  * Create the parser for one import call.
@@ -1119,6 +1172,11 @@ class PajekParser {
1119
1172
  this.nodesCreated = false;
1120
1173
  this.verticesDone = false;
1121
1174
  this.sawLineSection = false;
1175
+ this.firstKind = "directed";
1176
+ this.directionSet = false;
1177
+ this.relationNames = /* @__PURE__ */ new Map();
1178
+ this.objects = [];
1179
+ this.objectHeader = false;
1122
1180
  this.indexOfPos = new Uint32Array(0);
1123
1181
  this.idOfPos = null;
1124
1182
  this.seen = new Uint8Array(0);
@@ -1152,9 +1210,11 @@ class PajekParser {
1152
1210
  if (isCommentOrBlank(text)) {
1153
1211
  return false;
1154
1212
  }
1213
+ const afterObjectHeader = this.objectHeader;
1214
+ this.objectHeader = false;
1155
1215
  try {
1156
1216
  if (isSectionLine(text)) {
1157
- this.header(text, line);
1217
+ this.header(text, line, afterObjectHeader);
1158
1218
  return this.ended;
1159
1219
  }
1160
1220
  switch (this.section) {
@@ -1176,6 +1236,9 @@ class PajekParser {
1176
1236
  case "matrix":
1177
1237
  this.matrixLine(text, line);
1178
1238
  return false;
1239
+ case "object":
1240
+ this.objectLine(text);
1241
+ return false;
1179
1242
  case "skip":
1180
1243
  return false;
1181
1244
  default: {
@@ -1209,7 +1272,10 @@ class PajekParser {
1209
1272
  "the file declares vertices but no *Arcs, *Edges, *Arcslist, *Edgeslist or *Matrix section"
1210
1273
  );
1211
1274
  this.resolver.setHeader(this.options.defaultDirected);
1275
+ } else if (!this.directionSet) {
1276
+ this.resolver.setHeader(this.firstKind === "directed");
1212
1277
  }
1278
+ const objects = this.writeObjects();
1213
1279
  if (this.coercer.mergeCount > 0) {
1214
1280
  this.report.warning(
1215
1281
  "coercion",
@@ -1217,21 +1283,72 @@ class PajekParser {
1217
1283
  `${this.coercer.mergeCount} label(s) merged into an id another label already produced under ids "number"`
1218
1284
  );
1219
1285
  }
1286
+ const pajek = {
1287
+ ...this.firstMode === null ? {} : { firstMode: this.firstMode },
1288
+ ...objects.length === 0 ? {} : { objects }
1289
+ };
1220
1290
  const meta = {
1221
1291
  sourceFormat: "pajek",
1222
1292
  ...this.networkName === null ? {} : { name: this.networkName },
1223
- ...this.firstMode === null ? {} : { extra: { pajek: { firstMode: this.firstMode } } }
1293
+ ...Object.keys(pajek).length === 0 ? {} : { extra: { pajek } }
1224
1294
  };
1225
1295
  this.sink.setMeta(meta);
1226
1296
  }
1297
+ /**
1298
+ * Write every `*Partition` / `*Vector` object into its node column: `partition` (i32) or
1299
+ * `vector` (f64), a later object of the same kind in `partition#2`, `vector#2`, ... The value
1300
+ * of vertex k is the object's k-th value.
1301
+ * @returns the objects' Pajek names and the columns they went to, for meta.extra.pajek.objects
1302
+ */
1303
+ writeObjects() {
1304
+ const used = /* @__PURE__ */ new Set();
1305
+ const written = [];
1306
+ for (const object of this.objects) {
1307
+ const where = { line: object.line };
1308
+ const partition = object.kind === "partition";
1309
+ const name = uniqueColumnName(partition ? PARTITION_COLUMN : VECTOR_COLUMN, null, (n2) => used.has(n2));
1310
+ used.add(name);
1311
+ const { handle, decl } = declareResolved(
1312
+ this.sink,
1313
+ "node",
1314
+ {
1315
+ name,
1316
+ dtype: partition ? "i32" : "f64",
1317
+ nullable: true,
1318
+ origin: { format: "pajek", id: object.kind }
1319
+ },
1320
+ this.report,
1321
+ where
1322
+ );
1323
+ const n = Math.min(object.values.length, this.vertexCount);
1324
+ for (let pos = 0; pos < n; pos++) {
1325
+ const value = object.values[pos];
1326
+ if (value !== null) {
1327
+ this.sink.setNodeValue(handle, this.indexOfPos[pos], value);
1328
+ }
1329
+ }
1330
+ const declared = object.count ?? object.values.length;
1331
+ if (declared !== this.vertexCount || object.values.length !== declared) {
1332
+ this.report.error(
1333
+ "validation-error",
1334
+ PAJEK_ISSUE.OBJECT_COUNT,
1335
+ `*${partition ? "Partition" : "Vector"} "${object.name}" declares ${declared} vertices and holds ${object.values.length} value(s); the network has ${this.vertexCount}`,
1336
+ where
1337
+ );
1338
+ }
1339
+ written.push({ kind: object.kind, name: object.name, column: decl.name });
1340
+ }
1341
+ return written;
1342
+ }
1227
1343
  // ============================================================ headers
1228
1344
  /**
1229
1345
  * A section header line.
1230
1346
  * @param text - the line
1231
1347
  * @param line - the line number
1348
+ * @param afterObjectHeader - the previous line was a `*Partition`, `*Vector` or skipped section header
1232
1349
  */
1233
- header(text, line) {
1234
- const skipping = this.section === "skip";
1350
+ header(text, line, afterObjectHeader) {
1351
+ const previous = this.section;
1235
1352
  this.endSection();
1236
1353
  const h = parseSectionHeader(text);
1237
1354
  if (h === null) {
@@ -1256,8 +1373,11 @@ class PajekParser {
1256
1373
  this.section = "none";
1257
1374
  return;
1258
1375
  case "vertices":
1259
- if (skipping && this.vertexCount >= 0) {
1260
- this.section = "skip";
1376
+ if (afterObjectHeader) {
1377
+ this.section = previous;
1378
+ if (previous === "object") {
1379
+ this.objects[this.objects.length - 1].count = h.count;
1380
+ }
1261
1381
  return;
1262
1382
  }
1263
1383
  if (this.vertexCount >= 0) {
@@ -1278,9 +1398,16 @@ class PajekParser {
1278
1398
  this.lineSection(h, line, "matrix", "directed");
1279
1399
  this.matrixRows = 0;
1280
1400
  return;
1401
+ case "partition":
1402
+ case "vector":
1403
+ this.objects.push({ kind: h.kind, name: h.name ?? "", line, count: null, values: [] });
1404
+ this.section = "object";
1405
+ this.objectHeader = true;
1406
+ return;
1281
1407
  case "unsupported":
1282
1408
  this.section = "skip";
1283
- this.report.error(
1409
+ this.objectHeader = true;
1410
+ this.report.warning(
1284
1411
  "unsupported",
1285
1412
  PAJEK_ISSUE.UNSUPPORTED_SECTION,
1286
1413
  `the *${h.keyword} section is not supported; its lines are skipped`,
@@ -1305,6 +1432,13 @@ class PajekParser {
1305
1432
  if (!Number.isSafeInteger(h.count)) {
1306
1433
  this.report.fail(PAJEK_ISSUE.VERTICES_COUNT, `*Vertices ${h.count}: not a count`, { line });
1307
1434
  }
1435
+ if (h.secondCount !== null && (h.secondCount < 0 || h.secondCount > h.count)) {
1436
+ this.report.fail(
1437
+ PAJEK_ISSUE.VERTICES_COUNT,
1438
+ `*Vertices ${h.count} ${h.secondCount}: the first-mode count is not within 0..${h.count}`,
1439
+ { line }
1440
+ );
1441
+ }
1308
1442
  try {
1309
1443
  this.sink.reserve(h.count);
1310
1444
  } catch (err) {
@@ -1333,8 +1467,9 @@ class PajekParser {
1333
1467
  }
1334
1468
  }
1335
1469
  /**
1336
- * A line section header: the vertex set is completed, the first one sets the sink's direction
1337
- * (rule 1 of design section 8.4), later ones only switch the kind.
1470
+ * A line section header: the vertex set is completed and the kind switched. The sink's
1471
+ * direction (rule 1 of design section 8.4) is set by the first LINE, not the first header, so
1472
+ * an empty `*Arcs` before the `*Edges` of an undirected network does not make it directed.
1338
1473
  * @param h - the header
1339
1474
  * @param line - the line number
1340
1475
  * @param section - the parser state for the section
@@ -1351,21 +1486,25 @@ class PajekParser {
1351
1486
  this.finishVertices();
1352
1487
  if (!this.sawLineSection) {
1353
1488
  this.sawLineSection = true;
1354
- this.resolver.setHeader(kind === "directed", { line });
1489
+ this.firstKind = kind;
1355
1490
  }
1356
1491
  this.section = section;
1357
1492
  this.kind = kind;
1358
1493
  this.relation = h.relation === null ? null : h.name ?? String(h.relation);
1494
+ if (h.relation !== null) {
1495
+ this.relationNames.set(h.relation, this.relation ?? String(h.relation));
1496
+ }
1359
1497
  }
1360
1498
  /**
1361
1499
  * Checks that run when a section ends.
1362
1500
  */
1363
1501
  endSection() {
1364
- if (this.section === "matrix" && this.matrixRows !== this.vertexCount) {
1502
+ const rows = this.firstMode ?? this.vertexCount;
1503
+ if (this.section === "matrix" && this.matrixRows !== rows) {
1365
1504
  this.report.error(
1366
1505
  "validation-error",
1367
1506
  PAJEK_ISSUE.MATRIX_ROWS,
1368
- `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${this.vertexCount}`
1507
+ `*Matrix has ${this.matrixRows} row(s); *Vertices declares ${rows}`
1369
1508
  );
1370
1509
  }
1371
1510
  this.section = "none";
@@ -1462,7 +1601,7 @@ class PajekParser {
1462
1601
  }
1463
1602
  const lines = this.seenCount + this.report.counts.skippedNodes;
1464
1603
  if (this.seenCount > 0 && lines < n) {
1465
- this.report.error(
1604
+ this.report.warning(
1466
1605
  "validation-error",
1467
1606
  PAJEK_ISSUE.VERTEX_COUNT,
1468
1607
  `*Vertices declares ${n} vertices but ${lines} vertex line(s) were read; the others have no label`
@@ -1498,8 +1637,9 @@ class PajekParser {
1498
1637
  );
1499
1638
  }
1500
1639
  let i = 1;
1501
- const label = tokens.length > 1 ? tokens[1] : null;
1502
- if (label !== null) {
1640
+ let label = null;
1641
+ if (tokens.length > 1 && !startsWithCoordinates(text, tokens)) {
1642
+ label = decodeCharacterReferences(tokens[1]);
1503
1643
  i = 2;
1504
1644
  }
1505
1645
  let x = 0;
@@ -1525,8 +1665,8 @@ class PajekParser {
1525
1665
  );
1526
1666
  }
1527
1667
  let shape = null;
1528
- if (i < tokens.length && SHAPES.has(tokens[i])) {
1529
- shape = tokens[i];
1668
+ if (i < tokens.length && isShapeKeyword(tokens[i])) {
1669
+ shape = tokens[i].toLowerCase();
1530
1670
  i++;
1531
1671
  }
1532
1672
  const extras = this.extras(tokens, i, "node");
@@ -1684,7 +1824,14 @@ class PajekParser {
1684
1824
  * @param line - the line number
1685
1825
  */
1686
1826
  edgeLine(text, line) {
1687
- const tokens = this.tokens(text, "edge");
1827
+ let tokens = this.tokens(text, "edge");
1828
+ let { relation } = this;
1829
+ const prefix = RELATION_PREFIX.exec(tokens[0]);
1830
+ if (prefix !== null) {
1831
+ const k = Number(prefix[1]);
1832
+ relation = this.relationNames.get(k) ?? String(k);
1833
+ tokens = tokens.slice(1);
1834
+ }
1688
1835
  if (tokens.length < 2 || !isVertexNumber(tokens[0]) || !isVertexNumber(tokens[1])) {
1689
1836
  this.report.counts.skippedEdges++;
1690
1837
  throw new LineError(
@@ -1722,17 +1869,18 @@ class PajekParser {
1722
1869
  if (valueText !== null && !valueIsWeight) {
1723
1870
  this.writeParameter("edge", VALUE_COLUMN, e, valueText);
1724
1871
  }
1725
- this.writeEdgeExtras(e, extras);
1872
+ this.writeEdgeExtras(e, extras, relation);
1726
1873
  }
1727
1874
  /**
1728
- * An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`.
1875
+ * An adjacency list line of `*Arcslist` / `*Edgeslist`: `v n1 n2 ...`; a negative vertex
1876
+ * number is read as its absolute value, as Pajek does.
1729
1877
  * @param text - the line
1730
1878
  * @param line - the line number
1731
1879
  */
1732
1880
  listLine(text, line) {
1733
1881
  const tokens = this.tokens(text, "edge");
1734
1882
  for (const token of tokens) {
1735
- if (!isVertexNumber(token)) {
1883
+ if (!LIST_VERTEX.test(token)) {
1736
1884
  this.report.counts.skippedEdges++;
1737
1885
  throw new LineError(
1738
1886
  "parse-error",
@@ -1741,15 +1889,17 @@ class PajekParser {
1741
1889
  );
1742
1890
  }
1743
1891
  }
1744
- const u = this.endpoint(Number(tokens[0]));
1892
+ const u = this.endpoint(Math.abs(Number(tokens[0])));
1745
1893
  for (let i = 1; i < tokens.length; i++) {
1746
- const v = this.endpoint(Number(tokens[i]));
1894
+ const v = this.endpoint(Math.abs(Number(tokens[i])));
1747
1895
  const e = this.pushEdge(u, v, void 0, line);
1748
1896
  this.writeRelation(e);
1749
1897
  }
1750
1898
  }
1751
1899
  /**
1752
- * A `*Matrix` row: N values, every non-zero entry an arc from the row's vertex with that value.
1900
+ * A `*Matrix` row, every non-zero entry an arc from the row's vertex with that value: N values
1901
+ * in a one-mode network; in a two-mode network (`*Vertices N N1`) the matrix is N1 rows of
1902
+ * N - N1 values, entry j of row i an arc from vertex i to vertex N1 + j.
1753
1903
  * @param text - the line
1754
1904
  * @param line - the line number
1755
1905
  */
@@ -1757,38 +1907,71 @@ class PajekParser {
1757
1907
  const tokens = this.tokens(text, "edge");
1758
1908
  const row = this.matrixRows;
1759
1909
  this.matrixRows++;
1760
- if (row >= this.vertexCount) {
1910
+ const offset = this.firstMode ?? 0;
1911
+ const rows = this.firstMode ?? this.vertexCount;
1912
+ const columns = this.vertexCount - offset;
1913
+ if (row >= rows) {
1761
1914
  throw new LineError(
1762
1915
  "validation-error",
1763
1916
  PAJEK_ISSUE.MATRIX_ROWS,
1764
- `*Matrix row ${row + 1} is beyond the ${this.vertexCount} declared vertices`
1917
+ `*Matrix row ${row + 1} is beyond the ${rows} declared row(s)`
1765
1918
  );
1766
1919
  }
1767
- if (tokens.length !== this.vertexCount) {
1920
+ if (tokens.length < columns) {
1768
1921
  throw new LineError(
1769
1922
  "parse-error",
1770
1923
  PAJEK_ISSUE.LINE,
1771
- `*Matrix row ${row + 1} has ${tokens.length} value(s); *Vertices declares ${this.vertexCount}`
1924
+ `*Matrix row ${row + 1} has ${tokens.length} value(s); the matrix has ${columns} column(s)`
1772
1925
  );
1773
1926
  }
1774
- for (const token of tokens) {
1775
- if (!isNumericText(token)) {
1927
+ for (let j = 0; j < columns; j++) {
1928
+ if (!isNumericText(tokens[j])) {
1776
1929
  throw new LineError(
1777
1930
  "parse-error",
1778
1931
  PAJEK_ISSUE.LINE,
1779
- `*Matrix row ${row + 1}: "${token}" is not a number`
1932
+ `*Matrix row ${row + 1}: "${tokens[j]}" is not a number`
1780
1933
  );
1781
1934
  }
1782
1935
  }
1783
- for (let j = 0; j < tokens.length; j++) {
1936
+ if (tokens.length > columns) {
1937
+ this.report.warnOnce(
1938
+ "validation-error",
1939
+ PAJEK_ISSUE.MATRIX_EXTRA,
1940
+ `*Matrix row ${row + 1} has ${tokens.length} value(s) for ${columns} column(s); the extra values are ignored`,
1941
+ { line }
1942
+ );
1943
+ }
1944
+ for (let j = 0; j < columns; j++) {
1784
1945
  const value = Number(tokens[j]);
1785
1946
  if (value === 0) {
1786
1947
  continue;
1787
1948
  }
1788
- const e = this.pushEdge(row, j, value, line);
1949
+ const e = this.pushEdge(row, offset + j, value, line);
1789
1950
  this.writeRelation(e);
1790
1951
  }
1791
1952
  }
1953
+ /**
1954
+ * A value line of a `*Partition` (integers) or `*Vector` (numbers): the values of the next
1955
+ * vertices, usually one per line.
1956
+ * @param text - the line
1957
+ */
1958
+ objectLine(text) {
1959
+ const object = this.objects[this.objects.length - 1];
1960
+ const tokens = this.tokens(text, "node");
1961
+ for (const token of tokens) {
1962
+ if (!isNumericText(token) || object.kind === "partition" && !isI32(Number(token))) {
1963
+ object.values.push(null);
1964
+ throw new LineError(
1965
+ "parse-error",
1966
+ PAJEK_ISSUE.LINE,
1967
+ `*${object.kind === "partition" ? "Partition" : "Vector"} "${object.name}": "${token}" is not ${object.kind === "partition" ? "an integer" : "a number"}`
1968
+ );
1969
+ }
1970
+ }
1971
+ for (const token of tokens) {
1972
+ object.values.push(Number(token));
1973
+ }
1974
+ }
1792
1975
  /**
1793
1976
  * Push one line through the direction resolver, counting the logical edges it produced.
1794
1977
  * @param u - the source position
@@ -1799,6 +1982,10 @@ class PajekParser {
1799
1982
  */
1800
1983
  pushEdge(u, v, weight, line) {
1801
1984
  const { sink } = this;
1985
+ if (!this.directionSet) {
1986
+ this.directionSet = true;
1987
+ this.resolver.setHeader(this.kind === "directed", { line });
1988
+ }
1802
1989
  const before = sink.edgeCount;
1803
1990
  const e = this.resolver.addEdge(this.idAt(u), this.idAt(v), this.kind, weight, { line });
1804
1991
  this.report.counts.edges += sink.edgeCount - before;
@@ -1808,10 +1995,11 @@ class PajekParser {
1808
1995
  * Write the relation, parameters and spells of a line on its primary half.
1809
1996
  * @param e - the edge index
1810
1997
  * @param extras - the parsed extras
1998
+ * @param relation - the line's relation name, or null
1811
1999
  */
1812
- writeEdgeExtras(e, extras) {
2000
+ writeEdgeExtras(e, extras, relation) {
1813
2001
  const { sink } = this;
1814
- this.writeRelation(e);
2002
+ this.writeRelation(e, relation);
1815
2003
  for (let p = 0; p < extras.keys.length; p++) {
1816
2004
  this.writeParameter("edge", extras.keys[p], e, extras.values[p]);
1817
2005
  }
@@ -1823,17 +2011,18 @@ class PajekParser {
1823
2011
  }
1824
2012
  }
1825
2013
  /**
1826
- * Write the current section's relation name on an edge, when the section has one.
2014
+ * Write a relation name on an edge: the current section's, unless a line prefix named another.
1827
2015
  * @param e - the edge index
2016
+ * @param relation - the relation name, or null for none
1828
2017
  */
1829
- writeRelation(e) {
1830
- if (this.relation === null) {
2018
+ writeRelation(e, relation = this.relation) {
2019
+ if (relation === null) {
1831
2020
  return;
1832
2021
  }
1833
2022
  if (this.relationHandle === INVALID_INDEX) {
1834
2023
  this.relationHandle = declareResolved(this.sink, "edge", RELATION_DECL, this.report).handle;
1835
2024
  }
1836
- this.sink.setEdgeValue(this.relationHandle, e, this.relation);
2025
+ this.sink.setEdgeValue(this.relationHandle, e, relation);
1837
2026
  }
1838
2027
  /**
1839
2028
  * A line endpoint: the vertex number checked against the declared range; a reference outside
@@ -1900,7 +2089,8 @@ class PajekParser {
1900
2089
  const token = tokens[i];
1901
2090
  if (isIntervalToken(token)) {
1902
2091
  try {
1903
- extras.spells = parseIntervals(token);
2092
+ const spells = parseIntervals(token);
2093
+ extras.spells = spells.length === 0 ? null : spells;
1904
2094
  } catch (err) {
1905
2095
  this.skip(domain);
1906
2096
  throw new LineError(
@@ -1961,6 +2151,16 @@ class PajekParser {
1961
2151
  writer.write(row, text);
1962
2152
  }
1963
2153
  }
2154
+ function startsWithCoordinates(text, tokens) {
2155
+ if (text.trimStart().slice(tokens[0].length).trimStart().startsWith('"')) {
2156
+ return false;
2157
+ }
2158
+ let run = 0;
2159
+ while (1 + run < tokens.length && isNumericText(tokens[1 + run])) {
2160
+ run++;
2161
+ }
2162
+ return run === 2;
2163
+ }
1964
2164
  function firstVertexOption(value) {
1965
2165
  if (value === void 0) {
1966
2166
  return "auto";
@@ -2040,7 +2240,8 @@ const pajekImporter = Object.freeze({
2040
2240
  },
2041
2241
  /**
2042
2242
  * Read every network of a Pajek project file (`.paj`), each into its own sink. A network starts
2043
- * at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section.
2243
+ * at `*Network` or at a `*Vertices` that does not belong to a `*Partition` / `*Vector` section
2244
+ * (the `*Vertices N` line right after an object's header).
2044
2245
  * @param input - the text, bytes or stream
2045
2246
  * @param sinkFor - the sink of the network with this index, called before its first push
2046
2247
  * @param options - common and Pajek options
@@ -2082,27 +2283,35 @@ class NetworkCounter {
2082
2283
  this.count = 0;
2083
2284
  this.named = false;
2084
2285
  this.populated = false;
2085
- this.skipping = false;
2286
+ this.afterObjectHeader = false;
2086
2287
  }
2087
2288
  /**
2088
2289
  * Feed one line.
2089
2290
  * @param text - the line
2090
2291
  */
2091
2292
  line(text) {
2293
+ if (isCommentOrBlank(text)) {
2294
+ return;
2295
+ }
2296
+ const { afterObjectHeader } = this;
2297
+ this.afterObjectHeader = false;
2092
2298
  if (!isSectionLine(text)) {
2093
2299
  return;
2094
2300
  }
2095
2301
  const h = parseSectionHeader(text);
2096
- const kind = h === null ? "unsupported" : h.kind;
2097
- const { skipping } = this;
2098
- this.skipping = kind === "unsupported" || kind === "vertices" && skipping && this.populated;
2099
- if (kind === "network") {
2302
+ if (h === null) {
2303
+ return;
2304
+ }
2305
+ const { kind } = h;
2306
+ if (kind === "partition" || kind === "vector" || kind === "unsupported") {
2307
+ this.afterObjectHeader = true;
2308
+ } else if (kind === "network") {
2100
2309
  if (this.count === 0 || this.named || this.populated) {
2101
2310
  this.count++;
2102
2311
  this.populated = false;
2103
2312
  }
2104
2313
  this.named = true;
2105
- } else if (kind === "vertices" && !this.skipping) {
2314
+ } else if (kind === "vertices" && !afterObjectHeader) {
2106
2315
  if (this.count === 0 || this.populated) {
2107
2316
  this.count++;
2108
2317
  this.named = false;
@@ -2117,4 +2326,4 @@ export {
2117
2326
  pajekImporter as b,
2118
2327
  pajekExporter as p
2119
2328
  };
2120
- //# sourceMappingURL=importer-C2caVvl6.js.map
2329
+ //# sourceMappingURL=importer-DfI8POXb.js.map