@graphty/graph-io 0.3.9 → 0.3.11

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 (125) hide show
  1. package/README.md +29 -5
  2. package/dist/chunks/{escape-BjmIaFFo.js → escape-D1f9-cwf.js} +4 -4
  3. package/dist/chunks/{escape-BjmIaFFo.js.map → escape-D1f9-cwf.js.map} +1 -1
  4. package/dist/chunks/{importer-BddBW3sf.js → importer-9PxhqH4v.js} +336 -92
  5. package/dist/chunks/importer-9PxhqH4v.js.map +1 -0
  6. package/dist/chunks/{importer-DfI8POXb.js → importer-B8lsjFWx.js} +6 -5
  7. package/dist/chunks/importer-B8lsjFWx.js.map +1 -0
  8. package/dist/chunks/{importer-BtHFICnf.js → importer-C3HcYWAn.js} +5 -3
  9. package/dist/chunks/importer-C3HcYWAn.js.map +1 -0
  10. package/dist/chunks/{importer-BOxwmef_.js → importer-C7mnGdr_.js} +125 -8
  11. package/dist/chunks/importer-C7mnGdr_.js.map +1 -0
  12. package/dist/chunks/{records-BKSMowhR.js → records-IHsCfv7s.js} +20 -7
  13. package/dist/chunks/records-IHsCfv7s.js.map +1 -0
  14. package/dist/chunks/{writer-BdMak4_J.js → writer-BtWpUaiH.js} +4 -4
  15. package/dist/chunks/{writer-BdMak4_J.js.map → writer-BtWpUaiH.js.map} +1 -1
  16. package/dist/csv.js +239 -18
  17. package/dist/csv.js.map +1 -1
  18. package/dist/dot.js +1 -1
  19. package/dist/gexf.js +72 -33
  20. package/dist/gexf.js.map +1 -1
  21. package/dist/gml.js +44 -23
  22. package/dist/gml.js.map +1 -1
  23. package/dist/graph-io.js +11 -11
  24. package/dist/graphml.js +1 -1
  25. package/dist/json.js +1 -1
  26. package/dist/neo4j.js +5 -4
  27. package/dist/neo4j.js.map +1 -1
  28. package/dist/pajek.js +1 -1
  29. package/dist/src/common/export.js +1 -1
  30. package/dist/src/common/export.js.map +1 -1
  31. package/dist/src/formats/csv/exporter.d.ts +15 -4
  32. package/dist/src/formats/csv/exporter.d.ts.map +1 -1
  33. package/dist/src/formats/csv/exporter.js +125 -6
  34. package/dist/src/formats/csv/exporter.js.map +1 -1
  35. package/dist/src/formats/csv/importer.d.ts +36 -4
  36. package/dist/src/formats/csv/importer.d.ts.map +1 -1
  37. package/dist/src/formats/csv/importer.js +169 -8
  38. package/dist/src/formats/csv/importer.js.map +1 -1
  39. package/dist/src/formats/csv/records.d.ts +14 -1
  40. package/dist/src/formats/csv/records.d.ts.map +1 -1
  41. package/dist/src/formats/csv/records.js +28 -7
  42. package/dist/src/formats/csv/records.js.map +1 -1
  43. package/dist/src/formats/dot/exporter.d.ts.map +1 -1
  44. package/dist/src/formats/dot/exporter.js +2 -0
  45. package/dist/src/formats/dot/exporter.js.map +1 -1
  46. package/dist/src/formats/gexf/exporter.d.ts.map +1 -1
  47. package/dist/src/formats/gexf/exporter.js +36 -10
  48. package/dist/src/formats/gexf/exporter.js.map +1 -1
  49. package/dist/src/formats/gexf/importer.d.ts +9 -4
  50. package/dist/src/formats/gexf/importer.d.ts.map +1 -1
  51. package/dist/src/formats/gexf/importer.js +44 -21
  52. package/dist/src/formats/gexf/importer.js.map +1 -1
  53. package/dist/src/formats/gexf/index.d.ts +1 -1
  54. package/dist/src/formats/gexf/index.d.ts.map +1 -1
  55. package/dist/src/formats/gexf/index.js +1 -1
  56. package/dist/src/formats/gexf/index.js.map +1 -1
  57. package/dist/src/formats/gexf/schema.d.ts +2 -0
  58. package/dist/src/formats/gexf/schema.d.ts.map +1 -1
  59. package/dist/src/formats/gexf/schema.js +18 -0
  60. package/dist/src/formats/gexf/schema.js.map +1 -1
  61. package/dist/src/formats/gml/exporter.d.ts.map +1 -1
  62. package/dist/src/formats/gml/exporter.js +1 -0
  63. package/dist/src/formats/gml/exporter.js.map +1 -1
  64. package/dist/src/formats/gml/importer.d.ts +7 -4
  65. package/dist/src/formats/gml/importer.d.ts.map +1 -1
  66. package/dist/src/formats/gml/importer.js +32 -19
  67. package/dist/src/formats/gml/importer.js.map +1 -1
  68. package/dist/src/formats/gml/index.d.ts +3 -1
  69. package/dist/src/formats/gml/index.d.ts.map +1 -1
  70. package/dist/src/formats/gml/index.js +4 -2
  71. package/dist/src/formats/gml/index.js.map +1 -1
  72. package/dist/src/formats/graphml/constants.d.ts +2 -0
  73. package/dist/src/formats/graphml/constants.d.ts.map +1 -1
  74. package/dist/src/formats/graphml/constants.js +2 -0
  75. package/dist/src/formats/graphml/constants.js.map +1 -1
  76. package/dist/src/formats/graphml/exporter.d.ts.map +1 -1
  77. package/dist/src/formats/graphml/exporter.js +14 -2
  78. package/dist/src/formats/graphml/exporter.js.map +1 -1
  79. package/dist/src/formats/graphml/importer.d.ts +2 -1
  80. package/dist/src/formats/graphml/importer.d.ts.map +1 -1
  81. package/dist/src/formats/graphml/importer.js +91 -27
  82. package/dist/src/formats/graphml/importer.js.map +1 -1
  83. package/dist/src/formats/graphml/yfiles.d.ts +62 -0
  84. package/dist/src/formats/graphml/yfiles.d.ts.map +1 -0
  85. package/dist/src/formats/graphml/yfiles.js +269 -0
  86. package/dist/src/formats/graphml/yfiles.js.map +1 -0
  87. package/dist/src/formats/json/exporter.d.ts.map +1 -1
  88. package/dist/src/formats/json/exporter.js +9 -1
  89. package/dist/src/formats/json/exporter.js.map +1 -1
  90. package/dist/src/formats/json/importer.d.ts +14 -0
  91. package/dist/src/formats/json/importer.d.ts.map +1 -1
  92. package/dist/src/formats/json/importer.js +165 -4
  93. package/dist/src/formats/json/importer.js.map +1 -1
  94. package/dist/src/formats/neo4j/exporter.d.ts.map +1 -1
  95. package/dist/src/formats/neo4j/exporter.js +1 -0
  96. package/dist/src/formats/neo4j/exporter.js.map +1 -1
  97. package/dist/src/formats/pajek/exporter.d.ts.map +1 -1
  98. package/dist/src/formats/pajek/exporter.js +3 -2
  99. package/dist/src/formats/pajek/exporter.js.map +1 -1
  100. package/package.json +2 -2
  101. package/src/common/export.ts +1 -1
  102. package/src/formats/csv/exporter.ts +140 -10
  103. package/src/formats/csv/importer.ts +207 -18
  104. package/src/formats/csv/records.ts +41 -6
  105. package/src/formats/dot/exporter.ts +2 -0
  106. package/src/formats/gexf/exporter.ts +40 -11
  107. package/src/formats/gexf/importer.ts +47 -25
  108. package/src/formats/gexf/index.ts +1 -1
  109. package/src/formats/gexf/schema.ts +18 -0
  110. package/src/formats/gml/exporter.ts +1 -0
  111. package/src/formats/gml/importer.ts +47 -24
  112. package/src/formats/gml/index.ts +4 -1
  113. package/src/formats/graphml/constants.ts +2 -0
  114. package/src/formats/graphml/exporter.ts +21 -2
  115. package/src/formats/graphml/importer.ts +99 -28
  116. package/src/formats/graphml/yfiles.ts +292 -0
  117. package/src/formats/json/exporter.ts +9 -1
  118. package/src/formats/json/importer.ts +202 -4
  119. package/src/formats/neo4j/exporter.ts +1 -0
  120. package/src/formats/pajek/exporter.ts +3 -1
  121. package/dist/chunks/importer-BOxwmef_.js.map +0 -1
  122. package/dist/chunks/importer-BddBW3sf.js.map +0 -1
  123. package/dist/chunks/importer-BtHFICnf.js.map +0 -1
  124. package/dist/chunks/importer-DfI8POXb.js.map +0 -1
  125. package/dist/chunks/records-BKSMowhR.js.map +0 -1
package/dist/csv.js CHANGED
@@ -1,8 +1,8 @@
1
- import { a$ as DICT_SAMPLE_ROWS, a_ as DictHeuristic, X as ROLE_TAKEN_CODE$1, o as ID_MERGED_CODE$1, i as DUPLICATE_EDGE_ID_CODE$1, k as DUPLICATE_NODE_CODE$1, y as MISSING_ID_CODE$1, M as MISSING_ENDPOINT_CODE$1, m as EMPTY_INPUT_CODE$1, aF as resolveImportOptions, I as ImportReportBuilder, aC as reportSinkOptions, aD as reportUnusedOptions, l as DirectionResolver, v as IdCoercer, a as throwIfAborted, aA as parseWeightText, aL as uniqueColumnName, U as RENAMED_CODE, ai as declareResolved, ac as capabilities, L as LOSS, ax as joinText, ak as encodeChunks, ad as checkCapabilities, aE as resolveExportOptions, az as pairFolding, aJ as countMixedEdges, al as explicitWeights, aY as joinListText, aq as formatInteger, am as formatDecimal, ab as canonicalId, z as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, Y as SINK_OPTION_CODE, P as OPTION_IGNORED_CODE, C as COLUMN_RENAMED_CODE, a5 as UNKNOWN_ENCODING_CODE, n as ENCODING_FALLBACK_CODE, s as INVALID_ENCODING_CODE, u as INVALID_UTF8_CODE } from "./chunks/writer-BdMak4_J.js";
1
+ import { a$ as DICT_SAMPLE_ROWS, a_ as DictHeuristic, X as ROLE_TAKEN_CODE$1, o as ID_MERGED_CODE$1, i as DUPLICATE_EDGE_ID_CODE$1, k as DUPLICATE_NODE_CODE$1, y as MISSING_ID_CODE$1, M as MISSING_ENDPOINT_CODE$1, m as EMPTY_INPUT_CODE$1, aF as resolveImportOptions, I as ImportReportBuilder, aC as reportSinkOptions, aD as reportUnusedOptions, l as DirectionResolver, v as IdCoercer, a as throwIfAborted, aA as parseWeightText, aL as uniqueColumnName, U as RENAMED_CODE, ai as declareResolved, ac as capabilities, L as LOSS, ax as joinText, ak as encodeChunks, ad as checkCapabilities, aE as resolveExportOptions, az as pairFolding, aJ as countMixedEdges, al as explicitWeights, aY as joinListText, aq as formatInteger, am as formatDecimal, ab as canonicalId, z as MIXED_DIRECTION_CODE, g as DIRECTION_FORCED_CODE, h as DIRECTION_REFUSED_CODE, Y as SINK_OPTION_CODE, P as OPTION_IGNORED_CODE, C as COLUMN_RENAMED_CODE, a5 as UNKNOWN_ENCODING_CODE, n as ENCODING_FALLBACK_CODE, s as INVALID_ENCODING_CODE, u as INVALID_UTF8_CODE } from "./chunks/writer-BtWpUaiH.js";
2
2
  import { i as inferTextDtype, T as TextCellWriter, p as parseTextCell, W as WIDENING_UNSUPPORTED_CODE } from "./chunks/text-Dr0Ifpag.js";
3
3
  import { GraphFormatError, INVALID_INDEX } from "@graphty/graph-format";
4
- import { s as sniffNewline, a as sniffDelimiter, C as CsvRecordReader, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-BKSMowhR.js";
5
- import { n as quoteCsvCell } from "./chunks/escape-BjmIaFFo.js";
4
+ import { s as sniffNewline, a as sniffDelimiter, C as CsvRecordReader, D as DELIMITER_CANDIDATES, B as BAD_QUOTE_CODE, U as UNCLOSED_QUOTE_CODE } from "./chunks/records-IHsCfv7s.js";
5
+ import { m as quoteCsvCell } from "./chunks/escape-D1f9-cwf.js";
6
6
  const SOURCE_NAMES = Object.freeze([
7
7
  "source",
8
8
  "src",
@@ -277,7 +277,7 @@ const ID_MERGED_CODE = ID_MERGED_CODE$1;
277
277
  const COLUMN_MISSING_CODE = "W_CSV_COLUMN_MISSING";
278
278
  const ROLE_TAKEN_CODE = ROLE_TAKEN_CODE$1;
279
279
  const DUPLICATE_EDGE_ID_CODE = DUPLICATE_EDGE_ID_CODE$1;
280
- const TABLE_MODES = /* @__PURE__ */ new Set(["edges", "nodes", "auto"]);
280
+ const TABLE_MODES = /* @__PURE__ */ new Set(["edges", "nodes", "adjacency", "auto"]);
281
281
  const USED_OPTIONS = /* @__PURE__ */ new Set([
282
282
  "ids",
283
283
  "addMissingNodes",
@@ -330,7 +330,7 @@ function resolveCsvOptions(options) {
330
330
  });
331
331
  }
332
332
  if (o.table !== void 0 && !TABLE_MODES.has(o.table)) {
333
- throw new GraphFormatError("E_UNSUPPORTED", 'option table: expected "edges", "nodes" or "auto"', {
333
+ throw new GraphFormatError("E_UNSUPPORTED", 'option table: expected "edges", "nodes", "adjacency" or "auto"', {
334
334
  option: "table",
335
335
  found: o.table
336
336
  });
@@ -341,6 +341,28 @@ function resolveCsvOptions(options) {
341
341
  if (o.typeColumn !== null) {
342
342
  checkColumnRef("typeColumn", o.typeColumn);
343
343
  }
344
+ if (o.table === "adjacency") {
345
+ for (const name of ["sourceColumn", "targetColumn", "typeColumn", "idColumn"]) {
346
+ if (o[name] !== void 0) {
347
+ throw new GraphFormatError("E_UNSUPPORTED", `option ${name}: an adjacency table has no columns`, {
348
+ option: name,
349
+ found: o[name]
350
+ });
351
+ }
352
+ }
353
+ if (o.rowNumberIds === true) {
354
+ throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: an adjacency table names its nodes", {
355
+ option: "rowNumberIds",
356
+ found: o.rowNumberIds
357
+ });
358
+ }
359
+ }
360
+ if (o.rowNumberIds !== void 0 && typeof o.rowNumberIds !== "boolean") {
361
+ throw new GraphFormatError("E_UNSUPPORTED", "option rowNumberIds: expected a boolean", {
362
+ option: "rowNumberIds",
363
+ found: o.rowNumberIds
364
+ });
365
+ }
344
366
  return {
345
367
  delimiter: o.delimiter ?? null,
346
368
  header: o.header ?? "auto",
@@ -349,7 +371,8 @@ function resolveCsvOptions(options) {
349
371
  targetColumn: o.targetColumn ?? null,
350
372
  typeColumn: o.typeColumn,
351
373
  idColumn: o.idColumn ?? null,
352
- nodes: o.nodes ?? null
374
+ nodes: o.nodes ?? null,
375
+ rowNumberIds: o.rowNumberIds ?? false
353
376
  };
354
377
  }
355
378
  function checkColumnRef(name, value) {
@@ -417,6 +440,25 @@ function parseKind(text) {
417
440
  return null;
418
441
  }
419
442
  }
443
+ function splitNeighbour(text) {
444
+ const colon = text.lastIndexOf(":");
445
+ if (colon < 0) {
446
+ return { id: text, weight: void 0 };
447
+ }
448
+ const suffix = text.slice(colon + 1);
449
+ if (suffix.length === 0) {
450
+ return { id: text.slice(0, colon), weight: void 0 };
451
+ }
452
+ const weight = numberOrNull(suffix);
453
+ return weight === null ? { id: text, weight: void 0 } : { id: text.slice(0, colon), weight };
454
+ }
455
+ function numberOrNull(text) {
456
+ try {
457
+ return parseWeightText(text) ?? null;
458
+ } catch {
459
+ return null;
460
+ }
461
+ }
420
462
  class TableReader {
421
463
  /**
422
464
  * Create a table reader.
@@ -436,9 +478,12 @@ class TableReader {
436
478
  this.where = { line: null, element: null };
437
479
  this.state = state;
438
480
  this.kind = kind;
481
+ this.rowNumbers = state.csv.rowNumberIds && (kind === "nodes" || kind === "auto" && state.csv.nodes === null);
439
482
  const readerOptions = {
440
483
  delimiter: state.csv.delimiter,
441
484
  comments: COMMENT_CHARS,
485
+ // field counts say nothing about an adjacency table, whose rows vary in width
486
+ skipQuoteErrors: kind === "adjacency",
442
487
  signal: state.common.signal,
443
488
  onProgress: progress ? state.common.onProgress : null,
444
489
  encoding: state.common.encoding
@@ -463,12 +508,20 @@ class TableReader {
463
508
  async readRows(iterator) {
464
509
  const { report } = this.state;
465
510
  const first = await iterator.next();
511
+ if (first.done && this.kind === "adjacency") {
512
+ return;
513
+ }
466
514
  const firstRow = first.done ? report.fail(EMPTY_INPUT_CODE, "the input is empty: no header row and no records") : first.value;
467
515
  const firstLine = this.reader.line;
468
516
  const firstQuoted = this.reader.quoted.slice(0, firstRow.length);
469
517
  const pending = [];
470
518
  let header;
471
- const { header: mode } = this.state.csv;
519
+ let mode = this.state.csv.header;
520
+ if (mode === "auto" && this.kind === "adjacency") {
521
+ mode = false;
522
+ } else if (mode === "auto" && this.rowNumbers) {
523
+ mode = true;
524
+ }
472
525
  if (mode === "auto") {
473
526
  const second = await iterator.next();
474
527
  const secondRow = second.done ? null : second.value;
@@ -527,6 +580,9 @@ class TableReader {
527
580
  */
528
581
  resolvePlan(names, header, line) {
529
582
  const { csv, report } = this.state;
583
+ if (this.kind === "adjacency") {
584
+ return { kind: "adjacency", names: [] };
585
+ }
530
586
  const width = names.length;
531
587
  let source = -1;
532
588
  let target = -1;
@@ -565,7 +621,7 @@ class TableReader {
565
621
  );
566
622
  }
567
623
  const idResolves = header ? findColumn(names, ID_NAMES) >= 0 : width >= 1;
568
- if (this.kind === "auto" && csv.idColumn === null && !idResolves) {
624
+ if (this.kind === "auto" && csv.idColumn === null && !idResolves && !this.rowNumbers) {
569
625
  report.fail(
570
626
  NO_ENDPOINT_COLUMNS_CODE,
571
627
  `no source / target columns and no id column in the header (${shown}); the input is neither an edge table nor a node table`,
@@ -674,6 +730,10 @@ class TableReader {
674
730
  id = -1;
675
731
  break;
676
732
  default:
733
+ if (idColumn < 0 && this.rowNumbers) {
734
+ id = -1;
735
+ break;
736
+ }
677
737
  if (idColumn < 0) {
678
738
  report.fail(
679
739
  NO_ID_COLUMN_CODE,
@@ -695,7 +755,8 @@ class TableReader {
695
755
  attributes.push(i);
696
756
  }
697
757
  }
698
- return { kind: "nodes", names, width: names.length, id, label, attributes };
758
+ const rowNumber = id < 0 && common.nodeIdFrom !== "index";
759
+ return { kind: "nodes", names, width: names.length, id, rowNumber, label, attributes };
699
760
  }
700
761
  /**
701
762
  * Declare the role columns and create a writer per attribute column.
@@ -703,6 +764,9 @@ class TableReader {
703
764
  */
704
765
  prepareColumns(line) {
705
766
  const plan = this.requirePlan();
767
+ if (plan.kind === "adjacency") {
768
+ return;
769
+ }
706
770
  const { sink, report } = this.state;
707
771
  const domain = plan.kind === "edges" ? "edge" : "node";
708
772
  const origin = { format: "csv" };
@@ -752,8 +816,72 @@ class TableReader {
752
816
  this.dataRows++;
753
817
  if (plan.kind === "edges") {
754
818
  this.processEdgeRow(plan, row, quoted, line);
755
- } else {
819
+ } else if (plan.kind === "nodes") {
756
820
  this.processNodeRow(plan, row, quoted, line);
821
+ } else {
822
+ this.processAdjacencyRow(row, quoted, line);
823
+ }
824
+ }
825
+ /**
826
+ * Push one adjacency row: the node, then one edge per set neighbour cell, in row order.
827
+ * @param row - the cells
828
+ * @param quoted - whether each cell was quoted
829
+ * @param line - the row's line
830
+ */
831
+ processAdjacencyRow(row, quoted, line) {
832
+ const { report, sink, resolver, common } = this.state;
833
+ const { counts } = report;
834
+ const { where } = this;
835
+ where.line = line;
836
+ where.element = null;
837
+ let neighbours = 0;
838
+ for (let k = 1; k < row.length; k++) {
839
+ if (!isUnset(row[k], quoted[k])) {
840
+ neighbours++;
841
+ }
842
+ }
843
+ if (isUnset(row[0], quoted[0])) {
844
+ report.error("missing-value", MISSING_ID_CODE, `line ${line}: blank node cell`, { line });
845
+ counts.skippedNodes++;
846
+ counts.skippedEdges += neighbours;
847
+ return;
848
+ }
849
+ const kind = this.state.commentDirected ?? common.defaultDirected ? "directed" : "undirected";
850
+ if (!this.state.headerSet) {
851
+ this.state.headerSet = true;
852
+ resolver.setHeader(kind !== "undirected", where);
853
+ }
854
+ let source;
855
+ try {
856
+ where.element = row[0];
857
+ source = this.coerce(row[0]);
858
+ if (sink.indexOf(source) === INVALID_INDEX) {
859
+ counts.nodes++;
860
+ }
861
+ sink.addNode(source);
862
+ } catch (err) {
863
+ report.recordError(err, where);
864
+ counts.skippedNodes++;
865
+ counts.skippedEdges += neighbours;
866
+ return;
867
+ }
868
+ for (let k = 1; k < row.length; k++) {
869
+ if (isUnset(row[k], quoted[k])) {
870
+ continue;
871
+ }
872
+ where.element = row[k];
873
+ try {
874
+ const cell = splitNeighbour(row[k]);
875
+ const target = this.coerce(cell.id);
876
+ const targetNew = sink.indexOf(target) === INVALID_INDEX;
877
+ const before = sink.edgeCount;
878
+ resolver.addEdge(source, target, kind, common.weightFrom === null ? void 0 : cell.weight, where);
879
+ counts.edges += sink.edgeCount - before;
880
+ counts.nodes += targetNew ? 1 : 0;
881
+ } catch (err) {
882
+ report.recordError(err, where);
883
+ counts.skippedEdges++;
884
+ }
757
885
  }
758
886
  }
759
887
  /**
@@ -884,7 +1012,7 @@ class TableReader {
884
1012
  where.element = idText;
885
1013
  let index;
886
1014
  try {
887
- const id = plan.id >= 0 ? this.coerce(idText) : ordinal;
1015
+ const id = plan.id >= 0 || plan.rowNumber ? this.coerce(idText) : ordinal;
888
1016
  if (sink.indexOf(id) !== INVALID_INDEX) {
889
1017
  report.warning(
890
1018
  "merged",
@@ -1069,7 +1197,9 @@ const CSV_LOSS = Object.freeze({
1069
1197
  /** The edge table carries no node without an edge: isolated nodes vanish on re-import. */
1070
1198
  ISOLATED_NODES: "W_CSV_ISOLATED_NODES",
1071
1199
  /** The edge table lists nodes by first appearance; the node order (and indices) change on re-import. */
1072
- NODE_ORDER: "W_CSV_NODE_ORDER"
1200
+ NODE_ORDER: "W_CSV_NODE_ORDER",
1201
+ /** An adjacency table holds no edge column but the weight: edge ids, labels and attributes are not written. */
1202
+ EDGE_COLUMNS: "W_CSV_EDGE_COLUMNS"
1073
1203
  });
1074
1204
  const CSV_CAPABILITIES = capabilities({
1075
1205
  mixedDirection: true,
@@ -1102,7 +1232,8 @@ const SKIPPED_ROLES = /* @__PURE__ */ new Set([
1102
1232
  "timestamp",
1103
1233
  "timestamps",
1104
1234
  "spells",
1105
- "open"
1235
+ "open",
1236
+ "spellsOpen"
1106
1237
  ]);
1107
1238
  function resolveCsvExportOptions(options) {
1108
1239
  const o = options ?? {};
@@ -1112,8 +1243,8 @@ function resolveCsvExportOptions(options) {
1112
1243
  found: o.dialect
1113
1244
  });
1114
1245
  }
1115
- if (o.table !== void 0 && o.table !== "edges" && o.table !== "nodes") {
1116
- throw new GraphFormatError("E_UNSUPPORTED", 'option table: expected "edges" or "nodes"', {
1246
+ if (o.table !== void 0 && o.table !== "edges" && o.table !== "nodes" && o.table !== "adjacency") {
1247
+ throw new GraphFormatError("E_UNSUPPORTED", 'option table: expected "edges", "nodes" or "adjacency"', {
1117
1248
  option: "table",
1118
1249
  found: o.table
1119
1250
  });
@@ -1131,6 +1262,12 @@ function resolveCsvExportOptions(options) {
1131
1262
  found: o.newline
1132
1263
  });
1133
1264
  }
1265
+ if (o.table === "adjacency" && o.header === true) {
1266
+ throw new GraphFormatError("E_UNSUPPORTED", "option header: an adjacency table has no header row", {
1267
+ option: "header",
1268
+ found: o.header
1269
+ });
1270
+ }
1134
1271
  if (o.header !== void 0 && typeof o.header !== "boolean") {
1135
1272
  throw new GraphFormatError("E_UNSUPPORTED", "option header: expected a boolean", {
1136
1273
  option: "header",
@@ -1255,19 +1392,20 @@ function planExport(snapshot, options, values) {
1255
1392
  edgeRows.push(e);
1256
1393
  }
1257
1394
  }
1258
- if (csv.dialect.type === null) {
1395
+ if (csv.dialect.type === null || csv.table === "adjacency") {
1396
+ const where = csv.table === "adjacency" ? "an adjacency table" : "the generic dialect";
1259
1397
  const mixed = countMixedEdges(snapshot);
1260
1398
  if (!snapshot.directed) {
1261
1399
  note(
1262
1400
  CSV_LOSS.DIRECTION_DROPPED,
1263
- `the generic dialect has no direction column; ${snapshot.edgeCount} undirected edge(s) read back as directed unless the importer is told otherwise`,
1401
+ `${where} has no direction column; ${snapshot.edgeCount} undirected edge(s) read back as directed unless the importer is told otherwise`,
1264
1402
  null,
1265
1403
  snapshot.edgeCount
1266
1404
  );
1267
1405
  } else if (mixed > 0) {
1268
1406
  note(
1269
1407
  CSV_LOSS.DIRECTION_DROPPED,
1270
- `the generic dialect has no direction column; ${mixed} undirected edge(s) of a mixed graph read back as one directed edge each`,
1408
+ `${where} has no direction column; ${mixed} undirected edge(s) of a mixed graph read back as one directed edge each`,
1271
1409
  null,
1272
1410
  mixed
1273
1411
  );
@@ -1306,6 +1444,17 @@ function planExport(snapshot, options, values) {
1306
1444
  edgeLabel,
1307
1445
  csv.table === "edges" ? note : null
1308
1446
  );
1447
+ if (csv.table === "adjacency") {
1448
+ const dropped = edgeColumns.length + (edgeId === null ? 0 : 1) + (edgeLabel === null ? 0 : 1);
1449
+ if (dropped > 0) {
1450
+ note(
1451
+ CSV_LOSS.EDGE_COLUMNS,
1452
+ `${dropped} edge column(s) are not written: an adjacency table holds the weight only`,
1453
+ null,
1454
+ dropped
1455
+ );
1456
+ }
1457
+ }
1309
1458
  if (csv.table === "edges" && values) {
1310
1459
  checkRoleColumn(edgeId, "id", EDGE_ID_NAMES, "edge", edgeRows, note);
1311
1460
  checkRoleColumn(edgeLabel, "label", LABEL_NAMES, "edge", edgeRows, note);
@@ -1556,8 +1705,80 @@ function typeText(snapshot, plan, e) {
1556
1705
  }
1557
1706
  return "Directed";
1558
1707
  }
1708
+ function neighbourCell(id, weight) {
1709
+ if (weight !== null) {
1710
+ return `${id}:${weight}`;
1711
+ }
1712
+ return id.includes(":") ? `${id}:` : id;
1713
+ }
1714
+ function* adjacencyLines(snapshot, plan) {
1715
+ const { delimiter, newline } = plan.csv;
1716
+ const forced = (text) => `"${text.replace(/"/g, '""')}"`;
1717
+ const quote = (text) => DELIMITER_CANDIDATES.some((c) => text.includes(c)) ? forced(text) : quoteCsvCell(text, delimiter);
1718
+ const first = (text) => /^[#%]/.test(text) ? forced(text) : quote(text);
1719
+ const list = snapshot.edgeList();
1720
+ const { edgeRows, weights } = plan;
1721
+ const seen = new Uint8Array(snapshot.nodeCount);
1722
+ let next = 0;
1723
+ const advance = () => {
1724
+ while (next < seen.length && seen[next] === 1) {
1725
+ next++;
1726
+ }
1727
+ };
1728
+ function* singles(upTo) {
1729
+ for (let i = next; i <= upTo; i++) {
1730
+ if (seen[i] === 0) {
1731
+ seen[i] = 1;
1732
+ yield first(plan.idText(i)) + newline;
1733
+ }
1734
+ }
1735
+ advance();
1736
+ }
1737
+ let r = 0;
1738
+ while (r < edgeRows.length) {
1739
+ const source = list.src[edgeRows[r]];
1740
+ let end = r;
1741
+ while (end < edgeRows.length && list.src[edgeRows[end]] === source) {
1742
+ end++;
1743
+ }
1744
+ const fresh = [];
1745
+ let highest = -1;
1746
+ let inOrder = true;
1747
+ const mark = (v) => {
1748
+ if (seen[v] === 0) {
1749
+ seen[v] = 1;
1750
+ inOrder &&= v === next + fresh.length;
1751
+ fresh.push(v);
1752
+ highest = Math.max(highest, v);
1753
+ }
1754
+ };
1755
+ mark(source);
1756
+ for (let k = r; k < end; k++) {
1757
+ mark(list.dst[edgeRows[k]]);
1758
+ }
1759
+ if (!inOrder) {
1760
+ for (const v of fresh) {
1761
+ seen[v] = 0;
1762
+ }
1763
+ yield* singles(highest);
1764
+ }
1765
+ advance();
1766
+ const cells = [first(plan.idText(source))];
1767
+ for (let k = r; k < end; k++) {
1768
+ const e = edgeRows[k];
1769
+ cells.push(quote(neighbourCell(plan.idText(list.dst[e]), weights.weighted ? weights.text(e) : null)));
1770
+ }
1771
+ yield cells.join(delimiter) + newline;
1772
+ r = end;
1773
+ }
1774
+ yield* singles(snapshot.nodeCount - 1);
1775
+ }
1559
1776
  function* lines(snapshot, plan) {
1560
1777
  const { csv } = plan;
1778
+ if (csv.table === "adjacency") {
1779
+ yield* adjacencyLines(snapshot, plan);
1780
+ return;
1781
+ }
1561
1782
  const { delimiter, newline } = csv;
1562
1783
  const quote = (text) => quoteCsvCell(text, delimiter);
1563
1784
  const cell = (column, row) => {